diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 40bde6fa2..026e7b67e 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -4,6 +4,8 @@ on: push: branches: - features/* + - beta + - master pull_request: schedule: - cron: '0 2 * * *' @@ -158,6 +160,7 @@ jobs: fi echo VCMI_PACKAGE_FILE_NAME="$VCMI_PACKAGE_FILE_NAME" >> $GITHUB_ENV echo VCMI_PACKAGE_NAME_SUFFIX="$VCMI_PACKAGE_NAME_SUFFIX" >> $GITHUB_ENV + echo VCMI_PACKAGE_GITVERSION="$VCMI_PACKAGE_GITVERSION" >> $GITHUB_ENV env: PULL_REQUEST: ${{ github.event.pull_request.number }} @@ -171,7 +174,8 @@ jobs: ${{matrix.cmake_args}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ -DENABLE_TEST=${{matrix.test}} \ -DPACKAGE_NAME_SUFFIX:STRING="$VCMI_PACKAGE_NAME_SUFFIX" \ - -DPACKAGE_FILE_NAME:STRING="$VCMI_PACKAGE_FILE_NAME" + -DPACKAGE_FILE_NAME:STRING="$VCMI_PACKAGE_FILE_NAME" \ + -DENABLE_GITVERSION="$VCMI_PACKAGE_GITVERSION" env: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} @@ -222,7 +226,7 @@ jobs: ${{github.workspace}}/**/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }} - name: Upload build - if: ${{ matrix.pack == 1 && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' }} + if: ${{ matrix.pack == 1 && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' }} run: | cd '${{github.workspace}}/out/build/${{matrix.preset}}' source '${{github.workspace}}/CI/upload_package.sh' @@ -240,7 +244,7 @@ jobs: - name: Trigger Android uses: peter-evans/repository-dispatch@v1 - if: ${{ github.ref == 'refs/heads/develop' && matrix.platform == 'mxe' }} + if: ${{ (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && matrix.platform == 'mxe' }} with: token: ${{ secrets.VCMI_ANDROID_ACCESS_TOKEN }} repository: vcmi/vcmi-android diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 9b55e3578..b5ac226db 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -204,10 +204,14 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni if(targets.unreachableEnemies.empty()) return result; + auto speed = activeStack->Speed(); + + if(speed == 0) + return result; + updateReachabilityMap(hb); auto dists = cb->getReachability(activeStack); - auto speed = activeStack->Speed(); for(const battle::Unit * enemy : targets.unreachableEnemies) { diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index f587ae03a..01439809a 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -30,6 +30,7 @@ namespace NKAI // our to enemy strength ratio constants const float SAFE_ATTACK_CONSTANT = 1.2; const float RETREAT_THRESHOLD = 0.3; +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; @@ -500,10 +501,11 @@ boost::optional AIGateway::makeSurrenderRetreatDecision( LOG_TRACE(logAi); NET_EVENT_HANDLER; - double fightRatio = battleState.getOurStrength() / (double)battleState.getEnemyStrength(); + double ourStrength = battleState.getOurStrength(); + double fightRatio = ourStrength / (double)battleState.getEnemyStrength(); // if we have no towns - things are already bad, so retreat is not an option. - if(cb->getTownsInfo().size() && fightRatio < RETREAT_THRESHOLD && battleState.canFlee) + if(cb->getTownsInfo().size() && ourStrength < RETREAT_ABSOLUTE_THRESHOLD && fightRatio < RETREAT_THRESHOLD && battleState.canFlee) { return BattleAction::makeRetreat(battleState.ourSide); } @@ -533,6 +535,7 @@ void AIGateway::yourTurn() LOG_TRACE(logAi); NET_EVENT_HANDLER; status.startedTurn(); + makingTurn = make_unique(&AIGateway::makeTurn, this); } @@ -1426,7 +1429,15 @@ void AIGateway::endTurn() { logAi->error("Not having turn at the end of turn???"); } + logAi->debug("Resources at the end of turn: %s", cb->getResourceAmount().toString()); + + if(cb->getPlayerStatus(playerID) != EPlayerStatus::INGAME) + { + logAi->info("Ending turn is not needed because we already lost"); + return; + } + do { cb->endTurn(); @@ -1599,7 +1610,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.timed_wait(lock, boost::posix_time::milliseconds(10)); } bool AIStatus::haveTurn() diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 9c0dcd132..a20037d1b 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -74,6 +74,8 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const { + boost::this_thread::interruption_point(); + logAi->debug("Checking behavior %s", behavior->toString()); auto start = std::chrono::high_resolution_clock::now(); @@ -160,8 +162,12 @@ void Nullkiller::updateAiState(int pass, bool fast) cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT * ((int)scanDepth + 1); } + boost::this_thread::interruption_point(); + pathfinder->updatePaths(activeHeroes, cfg); + boost::this_thread::interruption_point(); + objectClusterizer->clusterize(); } @@ -212,6 +218,8 @@ HeroLockedReason Nullkiller::getHeroLockedReason(const CGHeroInstance * hero) co void Nullkiller::makeTurn() { + boost::lock_guard sharedStorageLock(AISharedStorage::locker); + const int MAX_DEPTH = 10; resetAiState(); diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 916ff8240..8b6361e3c 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -23,6 +23,7 @@ namespace NKAI { std::shared_ptr> AISharedStorage::shared; +boost::mutex AISharedStorage::locker; std::set commitedTiles; std::set commitedTilesInitial; @@ -119,18 +120,6 @@ void AINodeStorage::clear() turnDistanceLimit[HeroRole::SCOUT] = 255; } -const AIPathNode * AINodeStorage::getAINode(const CGPathNode * node) const -{ - return static_cast(node); -} - -void AINodeStorage::updateAINode(CGPathNode * node, std::function updater) -{ - auto aiNode = static_cast(node); - - updater(aiNode); -} - boost::optional AINodeStorage::getOrCreateNode( const int3 & pos, const EPathfindingLayer layer, @@ -823,13 +812,6 @@ ExchangeCandidate HeroChainCalculationTask::calculateExchange( return candidate; } -const CGHeroInstance * AINodeStorage::getHero(const CGPathNode * node) const -{ - auto aiNode = getAINode(node); - - return aiNode->actor->hero; -} - const std::set AINodeStorage::getAllHeroes() const { std::set heroes; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 008bff677..c1420afa5 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -135,6 +135,8 @@ class AISharedStorage static std::shared_ptr> shared; std::shared_ptr> nodes; public: + static boost::mutex locker; + AISharedStorage(int3 mapSize); ~AISharedStorage(); @@ -196,8 +198,24 @@ public: int movementLeft, float cost) const; - const AIPathNode * getAINode(const CGPathNode * node) const; - void updateAINode(CGPathNode * node, std::function updater); + inline const AIPathNode * getAINode(const CGPathNode * node) const + { + return static_cast(node); + } + + inline void updateAINode(CGPathNode * node, std::function updater) + { + auto aiNode = static_cast(node); + + updater(aiNode); + } + + inline const CGHeroInstance * getHero(const CGPathNode * node) const + { + auto aiNode = getAINode(node); + + return aiNode->actor->hero; + } bool hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const; @@ -223,18 +241,17 @@ public: void setTownsAndDwellings( const std::vector & towns, const std::set & visitableObjs); - const CGHeroInstance * getHero(const CGPathNode * node) const; const std::set getAllHeroes() const; void clear(); bool calculateHeroChain(); bool calculateHeroChainFinal(); - uint64_t evaluateDanger(const int3 & tile, const CGHeroInstance * hero, bool checkGuards) const + inline uint64_t evaluateDanger(const int3 & tile, const CGHeroInstance * hero, bool checkGuards) const { return dangerEvaluator->evaluateDanger(tile, hero, checkGuards); } - uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const + inline uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const { double ratio = (double)danger / (armyValue * hero->getFightingStrength()); @@ -243,6 +260,7 @@ public: STRONG_INLINE void resetTile(const int3 & tile, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility); + STRONG_INLINE int getBucket(const ChainActor * actor) const { return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT; diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp index 87eb5fb79..17cff54db 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp @@ -80,6 +80,8 @@ void AIPathfinder::updatePaths(std::map heroes do { + boost::this_thread::interruption_point(); + while(storage->calculateHeroChain()) { boost::this_thread::interruption_point(); @@ -91,6 +93,8 @@ void AIPathfinder::updatePaths(std::map heroes logAi->trace("Select next actor"); } while(storage->selectNextActor()); + boost::this_thread::interruption_point(); + if(storage->calculateHeroChainFinal()) { boost::this_thread::interruption_point(); diff --git a/AUTHORS b/AUTHORS index 079f813e3..6160a2eb4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -80,7 +80,7 @@ Dmitry Orlov, * special buildings support in fan towns, new features and bug fixes Andrey Cherkas aka nordsoft, - * new terrain support, random map generator features and various bug fixes + * 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/CI/get_package_name.sh b/CI/get_package_name.sh index 94554ff2e..235e39930 100644 --- a/CI/get_package_name.sh +++ b/CI/get_package_name.sh @@ -27,16 +27,27 @@ fi VCMI_PACKAGE_FILE_NAME="${TMP_JOBID}-vcmi" VCMI_PACKAGE_NAME_SUFFIX="" +VCMI_PACKAGE_GITVERSION="ON" if [ -z "$TMP_PRID" ] || [ "$TMP_PRID" == "false" ]; then branch_name=$(echo "$TMP_BRANCH" | sed 's/[^[:alnum:]]\+/_/g') VCMI_PACKAGE_FILE_NAME="${VCMI_PACKAGE_FILE_NAME}-branch-${branch_name}-${TMP_COMMIT}" - VCMI_PACKAGE_NAME_SUFFIX="branch ${branch_name}" + if [ "${branch_name}" != "master" ]; + then + VCMI_PACKAGE_NAME_SUFFIX="branch ${branch_name}" + else + VCMI_PACKAGE_GITVERSION="OFF" + fi else VCMI_PACKAGE_FILE_NAME="${VCMI_PACKAGE_FILE_NAME}-PR-${TMP_PRID}-${TMP_COMMIT}" VCMI_PACKAGE_NAME_SUFFIX="PR ${TMP_PRID}" fi -VCMI_PACKAGE_NAME_SUFFIX="(${VCMI_PACKAGE_NAME_SUFFIX})" + +if [ "${VCMI_PACKAGE_NAME_SUFFIX}" != "" ]; +then + VCMI_PACKAGE_NAME_SUFFIX="(${VCMI_PACKAGE_NAME_SUFFIX})" +fi export VCMI_PACKAGE_FILE_NAME export VCMI_PACKAGE_NAME_SUFFIX +export VCMI_PACKAGE_GITVERSION \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 14c6c5666..6fe46613f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -397,7 +397,6 @@ else() include(GNUInstallDirs) if(ENABLE_MONOLITHIC_INSTALL) - set(CMAKE_INSTALL_RPATH "$ORIGIN/") set(BIN_DIR "." CACHE STRING "Where to install binaries") set(LIB_DIR "." CACHE STRING "Where to install main library") set(DATA_DIR "." CACHE STRING "Where to install data files") @@ -406,21 +405,25 @@ else() add_definitions(-DM_DATA_DIR="${DATA_DIR}") add_definitions(-DM_BIN_DIR="${BIN_DIR}") add_definitions(-DM_LIB_DIR="${LIB_DIR}") + + set(CMAKE_INSTALL_RPATH "$ORIGIN/") else() if(NOT BIN_DIR) - set(BIN_DIR "bin" CACHE STRING "Where to install binaries") + set(BIN_DIR "${CMAKE_INSTALL_BINDIR}" CACHE STRING "Where to install binaries") endif() if(NOT LIB_DIR) set(LIB_DIR "${CMAKE_INSTALL_LIBDIR}/vcmi" CACHE STRING "Where to install main library") endif() if(NOT DATA_DIR) - set(DATA_DIR "share/vcmi" CACHE STRING "Where to install data files") + set(DATA_DIR "${CMAKE_INSTALL_DATAROOTDIR}/vcmi" CACHE STRING "Where to install data files") endif() # following constants only used for platforms using XDG (Linux, BSD, etc) add_definitions(-DM_DATA_DIR="${CMAKE_INSTALL_PREFIX}/${DATA_DIR}") add_definitions(-DM_BIN_DIR="${CMAKE_INSTALL_PREFIX}/${BIN_DIR}") add_definitions(-DM_LIB_DIR="${CMAKE_INSTALL_PREFIX}/${LIB_DIR}") + + set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_DIR}") endif() endif() diff --git a/CMakePresets.json b/CMakePresets.json index 04d49edf2..3ea47c488 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -23,7 +23,8 @@ "PACKAGE_FILE_NAME" : "$env{VCMI_PACKAGE_FILE_NAME}", "PACKAGE_NAME_SUFFIX" : "$env{VCMI_PACKAGE_NAME_SUFFIX}", "CMAKE_BUILD_TYPE": "RelWithDebInfo", - "ENABLE_TEST": "OFF" + "ENABLE_TEST": "OFF", + "ENABLE_GITVERSION": "$env{VCMI_PACKAGE_GITVERSION}" } }, { diff --git a/ChangeLog b/ChangeLog index d33b1499d..458aaf000 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,21 +1,69 @@ -1.0.0 -> 1.1.0 - -GENERAL: -* 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 - -MODS: -* Supported rewardable objects customization -* Battleground obstacles are extendable now with VLC mechanism -* Introduced "compatibility" section into mods settings - -LAUNCHER: -* Fixed problem with duplicated mods in the list -* Launcher shows compatible mods only -* Uninstall button was moved to the left of layout - +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: @@ -98,10 +146,10 @@ BATTLES: 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 +* 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 diff --git a/Mods/vcmi/config/vcmi/towerCreature.json b/Mods/vcmi/config/vcmi/towerCreature.json new file mode 100644 index 000000000..b62718b21 --- /dev/null +++ b/Mods/vcmi/config/vcmi/towerCreature.json @@ -0,0 +1,10 @@ +{ + "core:arrowTower" : + { + "graphics" : + { + "iconSmall" : "vcmi/creatureIcons/towerSmall", + "iconLarge" : "vcmi/creatureIcons/towerLarge", + } + } +} diff --git a/Mods/vcmi/config/vcmi/towerFactions.json b/Mods/vcmi/config/vcmi/towerFactions.json new file mode 100644 index 000000000..53845823b --- /dev/null +++ b/Mods/vcmi/config/vcmi/towerFactions.json @@ -0,0 +1,101 @@ +{ + "core:castle" : + { + "town" : + { + "siege" : + { + "towerIconSmall" : "vcmi/creatureIcons/towerSmall", + "towerIconLarge" : "vcmi/creatureIcons/towerLarge", + } + } + }, + "core:rampart" : + { + "town" : + { + "siege" : + { + "towerIconSmall" : "vcmi/creatureIcons/towerSmall", + "towerIconLarge" : "vcmi/creatureIcons/towerLarge", + } + } + }, + "core:tower" : + { + "town" : + { + "siege" : + { + "towerIconSmall" : "vcmi/creatureIcons/towerSmall", + "towerIconLarge" : "vcmi/creatureIcons/towerLarge", + } + } + }, + "core:inferno" : + { + "town" : + { + "siege" : + { + "towerIconSmall" : "vcmi/creatureIcons/towerSmall", + "towerIconLarge" : "vcmi/creatureIcons/towerLarge", + } + } + }, + "core:necropolis" : + { + "town" : + { + "siege" : + { + "towerIconSmall" : "vcmi/creatureIcons/towerSmall", + "towerIconLarge" : "vcmi/creatureIcons/towerLarge", + } + } + }, + "core:dungeon" : + { + "town" : + { + "siege" : + { + "towerIconSmall" : "vcmi/creatureIcons/towerSmall", + "towerIconLarge" : "vcmi/creatureIcons/towerLarge", + } + } + }, + "core:stronghold" : + { + "town" : + { + "siege" : + { + "towerIconSmall" : "vcmi/creatureIcons/towerSmall", + "towerIconLarge" : "vcmi/creatureIcons/towerLarge", + } + } + }, + "core:fortress" : + { + "town" : + { + "siege" : + { + "towerIconSmall" : "vcmi/creatureIcons/towerSmall", + "towerIconLarge" : "vcmi/creatureIcons/towerLarge", + } + } + }, + "core:conflux" : + { + "town" : + { + "siege" : + { + "towerIconSmall" : "vcmi/creatureIcons/towerSmall", + "towerIconLarge" : "vcmi/creatureIcons/towerLarge", + } + } + } +} diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index 5c07c3150..4a4b58ba8 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -2,13 +2,20 @@ "name" : "VCMI essential files", "description" : "Essential files required for VCMI to run correctly", - "version" : "1.0", + "version" : "1.1", "author" : "VCMI Team", "contact" : "http://forum.vcmi.eu/index.php", "modType" : "Graphical", + + "factions" : [ "config/vcmi/towerFactions" ], + "creatures" : [ "config/vcmi/towerCreature" ], "filesystem": { + "CONFIG/" : + [ + {"type" : "dir", "path" : "/Config"} + ], "DATA/" : [ {"type" : "dir", "path" : "/Data"} diff --git a/client/CMT.cpp b/client/CMT.cpp index 9977ac2c7..f983767e9 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -721,7 +721,7 @@ void processCommand(const std::string &message) { const JsonNode & object = nameAndObject.second; - std::string name = CModHandler::normalizeIdentifier(object.meta, "core", nameAndObject.first); + std::string name = CModHandler::normalizeIdentifier(object.meta, CModHandler::scopeBuiltin(), nameAndObject.first); boost::algorithm::replace_all(name,":","_"); diff --git a/client/Graphics.cpp b/client/Graphics.cpp index 19312c081..9bf835757 100644 --- a/client/Graphics.cpp +++ b/client/Graphics.cpp @@ -100,7 +100,7 @@ void Graphics::loadPaletteAndColors() void Graphics::initializeBattleGraphics() { auto allConfigs = VLC->modh->getActiveMods(); - allConfigs.insert(allConfigs.begin(), "core"); + allConfigs.insert(allConfigs.begin(), CModHandler::scopeBuiltin()); for(auto & mod : allConfigs) { if(!CResourceHandler::get(mod)->existsResource(ResourceID("config/battles_graphics.json"))) diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 3df6ac532..c9ba889e8 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -490,7 +490,11 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface int yPos = 344 + step * 97; for(auto & elem : br.casualties[step]) { - icons.push_back(std::make_shared("CPRSMALL", CGI->creatures()->getByIndex(elem.first)->getIconIndex(), 0, xPos, yPos)); + 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("CPRSMALL", creature->getIconIndex(), 0, xPos, yPos)); std::ostringstream amount; amount<(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str())); diff --git a/client/gui/CCursorHandler.cpp b/client/gui/CCursorHandler.cpp index 43e5999ad..af33ed182 100644 --- a/client/gui/CCursorHandler.cpp +++ b/client/gui/CCursorHandler.cpp @@ -67,6 +67,8 @@ void CCursorHandler::initCursor() void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index) { + assert(dndObject == nullptr); + if(type != this->type) { this->type = type; diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index 814fc3c07..48b3b3f6d 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -254,6 +254,22 @@ void CHeroArtPlace::clickRight(tribool down, bool previousState) } } +void CArtifactsOfHero::activate() +{ + if (commonInfo->src.AOH == this && commonInfo->src.art) + CCS->curh->dragAndDropCursor(make_unique("artifact", commonInfo->src.art->artType->getIconIndex())); + + CIntObject::activate(); +} + +void CArtifactsOfHero::deactivate() +{ + if (commonInfo->src.AOH == this && commonInfo->src.art) + CCS->curh->dragAndDropCursor(nullptr); + + CIntObject::deactivate(); +} + /** * Selects artifact slot so that the containing artifact looks like it's picked up. */ @@ -675,10 +691,8 @@ void CArtifactsOfHero::updateParentWindow() if(!updateState) { - cew->deactivate(); cew->updateWidgets(); cew->redraw(); - cew->activate(); } } } diff --git a/client/widgets/CArtifactHolder.h b/client/widgets/CArtifactHolder.h index 215d41d3a..36e16e547 100644 --- a/client/widgets/CArtifactHolder.h +++ b/client/widgets/CArtifactHolder.h @@ -148,6 +148,9 @@ public: void dispose(); //free resources not needed after closing windows and reset state void scrollBackpack(int dir); //dir==-1 => to left; dir==1 => to right + void activate() override; + void deactivate() override; + void safeRedraw(); void markPossibleSlots(const CArtifactInstance* art); void unmarkSlots(bool withRedraw = true); //unmarks slots in all visible AOHs diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 24ea71011..4fa8e46a4 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -676,6 +676,10 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta break; case EMarketMode::ARTIFACT_RESOURCE: title = (*CGI->townh)[market->o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->Name(); + + // 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 + images.push_back(std::make_shared(background->bg, Rect(20, 187, 47, 47), 18, 339 )); sliderNeeded = false; break; default: diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index a19a56a62..49bc5e74c 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -21,6 +21,7 @@ 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 @@ -108,6 +109,7 @@ public: protected: std::shared_ptr statusBar; std::vector> labels; + std::vector> images; std::vector> buttons; std::vector> texts; }; diff --git a/config/creatures/special.json b/config/creatures/special.json index 58ce4ab86..8b724bcc6 100644 --- a/config/creatures/special.json +++ b/config/creatures/special.json @@ -123,8 +123,6 @@ }, "graphics" : { - "iconSmall" : "vcmi/creatureIcons/towerSmall", - "iconLarge" : "vcmi/creatureIcons/towerLarge", "animation": "CLCBOW.DEF" // needed to pass validation, never used }, "sound": {} diff --git a/config/factions/castle.json b/config/factions/castle.json index 53a5c80a6..33a0006d8 100644 --- a/config/factions/castle.json +++ b/config/factions/castle.json @@ -203,8 +203,6 @@ "siege" : { "shooter" : "archer", - "towerIconSmall" : "vcmi/creatureIcons/towerSmall", - "towerIconLarge" : "vcmi/creatureIcons/towerLarge", "imagePrefix" : "SGCS", "gate" : { diff --git a/config/factions/conflux.json b/config/factions/conflux.json index 26ca39a6c..e63a770ad 100644 --- a/config/factions/conflux.json +++ b/config/factions/conflux.json @@ -210,8 +210,6 @@ "siege" : { "shooter" : "stormElemental", - "towerIconSmall" : "vcmi/creatureIcons/towerSmall", - "towerIconLarge" : "vcmi/creatureIcons/towerLarge", "imagePrefix" : "SGEL", "gate" : { diff --git a/config/factions/dungeon.json b/config/factions/dungeon.json index a47bf73f8..d03e436e6 100644 --- a/config/factions/dungeon.json +++ b/config/factions/dungeon.json @@ -204,8 +204,6 @@ "siege" : { "shooter" : "medusa", - "towerIconSmall" : "vcmi/creatureIcons/towerSmall", - "towerIconLarge" : "vcmi/creatureIcons/towerLarge", "imagePrefix" : "SGDN", "gate" : { diff --git a/config/factions/fortress.json b/config/factions/fortress.json index 5b89a7bcf..cdfd9fb8f 100644 --- a/config/factions/fortress.json +++ b/config/factions/fortress.json @@ -209,8 +209,6 @@ "siege" : { "shooter" : "lizardman", - "towerIconSmall" : "vcmi/creatureIcons/towerSmall", - "towerIconLarge" : "vcmi/creatureIcons/towerLarge", "imagePrefix" : "SGFR", "gate" : { diff --git a/config/factions/inferno.json b/config/factions/inferno.json index 8177bb212..bacb77cfa 100644 --- a/config/factions/inferno.json +++ b/config/factions/inferno.json @@ -204,8 +204,6 @@ "siege" : { "shooter" : "gog", - "towerIconSmall" : "vcmi/creatureIcons/towerSmall", - "towerIconLarge" : "vcmi/creatureIcons/towerLarge", "imagePrefix" : "SGIN", "gate" : { diff --git a/config/factions/necropolis.json b/config/factions/necropolis.json index e51c490d3..8caa848a1 100644 --- a/config/factions/necropolis.json +++ b/config/factions/necropolis.json @@ -214,8 +214,6 @@ "siege" : { "shooter" : "lich", - "towerIconSmall" : "vcmi/creatureIcons/towerSmall", - "towerIconLarge" : "vcmi/creatureIcons/towerLarge", "imagePrefix" : "SGNC", "gate" : { diff --git a/config/factions/rampart.json b/config/factions/rampart.json index 5da41b08e..c25deaec6 100644 --- a/config/factions/rampart.json +++ b/config/factions/rampart.json @@ -211,8 +211,6 @@ "siege" : { "shooter" : "woodElf", - "towerIconSmall" : "vcmi/creatureIcons/towerSmall", - "towerIconLarge" : "vcmi/creatureIcons/towerLarge", "imagePrefix" : "SGRM", "gate" : { diff --git a/config/factions/stronghold.json b/config/factions/stronghold.json index a66490551..d4c7fba71 100644 --- a/config/factions/stronghold.json +++ b/config/factions/stronghold.json @@ -203,8 +203,6 @@ { "shooter" : "orc", "imagePrefix" : "SGST", - "towerIconSmall" : "vcmi/creatureIcons/towerSmall", - "towerIconLarge" : "vcmi/creatureIcons/towerLarge", "gate" : { "arch" : { "x" : 478, "y" : 235 }, diff --git a/config/factions/tower.json b/config/factions/tower.json index fca3dc149..89e93b34a 100644 --- a/config/factions/tower.json +++ b/config/factions/tower.json @@ -202,8 +202,6 @@ "siege" : { "shooter" : "mage", - "towerIconSmall" : "vcmi/creatureIcons/towerSmall", - "towerIconLarge" : "vcmi/creatureIcons/towerLarge", "imagePrefix" : "SGTW", "gate" : { diff --git a/config/schemas/mod.json b/config/schemas/mod.json index 34aed68c3..012c3f762 100644 --- a/config/schemas/mod.json +++ b/config/schemas/mod.json @@ -56,6 +56,23 @@ "description": "List of mods that can't be enabled in the same time as this one", "items": { "type":"string" } }, + "compatibility" : { + "type":"object", + "description": "Supported versions of vcmi engine", + "additionalProperties" : false, + "properties" : { + "min" : { + "type" : "string", + "description" : "minimal compatible vcmi engine version in a format major.minor.patch. When specified, earlier versions won't be supported" + //"pattern" : "^\\d+\\.\\d+\\.\\d+$" // Not implemented in schema support + }, + "max" : { + "type" : "string", + "description" : "maximum compatible vcmi engine version in a format major.minor.patch. When specified, later versions won't be supported" + //"pattern" : "^\\d+\\.\\d+\\.\\d+$" // Not implemented in schema support + } + } + }, "keepDisabled" : { "type":"boolean", @@ -118,6 +135,12 @@ "description": "List of configuration files for battlefields", "items": { "type":"string", "format" : "textFile" } + }, + "obstacles":{ + "type":"array", + "description": "List of configuration files for obstacles", + "items": { "type":"string", "format" : "textFile" } + }, "changelog" : { diff --git a/debian/changelog b/debian/changelog index 845b3fff2..148a70e2f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +vcmi (1.1.0) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Fri, 23 Dec 2022 12:00:00 +0200 + vcmi (1.0.0) jammy; urgency=medium * New upstream release diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 9b5f82c40..227abec6c 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -38,6 +38,7 @@ https://github.com/vcmi/vcmi/issues https://vcmi.eu/faq/ + diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index 1be55a9e4..e08c99ce4 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -31,7 +31,7 @@ bool isCompatible(const QString & verMin, const QString & verMax) if(ver.segmentCount() < maxSections) { auto segments = ver.segments(); - for(int i = segments.size() - 1; i < maxSections; ++i) + for(int i = segments.size(); i < maxSections; ++i) segments.append(0); ver = QVersionNumber(segments); } diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 5a50c227d..0e0851639 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -280,7 +280,7 @@ std::vector CArtHandler::loadLegacyData(size_t dataSize) void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name), objects.size()); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name), objects.size()); object->iconIndex = object->getIndex() + 5; @@ -291,7 +291,7 @@ void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name), index); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name), index); object->iconIndex = object->getIndex(); @@ -861,8 +861,6 @@ void CArtifactInstance::removeFrom(ArtifactLocation al) al.getHolderArtSet()->eraseArtSlot(al.slot); if(!ArtifactUtils::isSlotBackpack(al.slot)) al.getHolderNode()->detachFrom(*this); - - //TODO delete me? } bool CArtifactInstance::canBeDisassembled() const @@ -1062,7 +1060,9 @@ void CCombinedArtifactInstance::createConstituents() void CCombinedArtifactInstance::addAsConstituent(CArtifactInstance *art, ArtifactPosition slot) { - assert(vstd::contains(*artType->constituents, art->artType.get())); + assert(vstd::contains_if(*artType->constituents, [=](const CArtifact * constituent){ + return constituent->id == art->artType->id; + })); assert(art->getParentNodes().size() == 1 && art->getParentNodes().front() == art->artType); constituentsInfo.push_back(ConstituentInfo(art, slot)); attachTo(*art); @@ -1354,11 +1354,16 @@ bool CArtifactSet::isPositionFree(ArtifactPosition pos, bool onlyLockCheck) cons ArtSlotInfo & CArtifactSet::retrieveNewArtSlot(ArtifactPosition slot) { assert(!vstd::contains(artifactsWorn, slot)); - ArtSlotInfo &ret = !ArtifactUtils::isSlotBackpack(slot) - ? artifactsWorn[slot] - : *artifactsInBackpack.insert(artifactsInBackpack.begin() + (slot - GameConstants::BACKPACK_START), ArtSlotInfo()); - return ret; + if (!ArtifactUtils::isSlotBackpack(slot)) + return artifactsWorn[slot]; + + ArtSlotInfo newSlot; + size_t index = slot - GameConstants::BACKPACK_START; + auto position = artifactsInBackpack.begin() + index; + auto inserted = artifactsInBackpack.insert(position, newSlot); + + return *inserted; } void CArtifactSet::setNewArtSlot(ArtifactPosition slot, CArtifactInstance *art, bool locked) @@ -1372,9 +1377,10 @@ void CArtifactSet::eraseArtSlot(ArtifactPosition slot) { if(ArtifactUtils::isSlotBackpack(slot)) { - assert(artifactsInBackpack.begin() + slot < artifactsInBackpack.end()); - slot = ArtifactPosition(slot - GameConstants::BACKPACK_START); - artifactsInBackpack.erase(artifactsInBackpack.begin() + slot); + auto backpackSlot = ArtifactPosition(slot - GameConstants::BACKPACK_START); + + assert(artifactsInBackpack.begin() + backpackSlot < artifactsInBackpack.end()); + artifactsInBackpack.erase(artifactsInBackpack.begin() + backpackSlot); } else { @@ -1547,16 +1553,18 @@ DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtifactDstPosition( const CArtif DLL_LINKAGE const std::vector & ArtifactUtils::unmovableSlots() { - return + static const std::vector positions = { ArtifactPosition::SPELLBOOK, ArtifactPosition::MACH4 }; + + return positions; } DLL_LINKAGE const std::vector & ArtifactUtils::constituentWornSlots() { - return + static const std::vector positions = { ArtifactPosition::HEAD, ArtifactPosition::SHOULDERS, @@ -1573,6 +1581,8 @@ DLL_LINKAGE const std::vector & ArtifactUti ArtifactPosition::MISC4, ArtifactPosition::MISC5, }; + + return positions; } DLL_LINKAGE bool ArtifactUtils::isArtRemovable(const std::pair & slot) diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 3a49ab0a7..e60fbeb51 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -425,7 +425,7 @@ const CCreature * CCreatureHandler::getCreature(const std::string & scope, const void CCreatureHandler::loadCommanders() { JsonNode data(ResourceID("config/commanders.json")); - data.setMeta("core"); // assume that commanders are in core mod (for proper bonuses resolution) + data.setMeta(CModHandler::scopeBuiltin()); // assume that commanders are in core mod (for proper bonuses resolution) const JsonNode & config = data; // switch to const data accessors diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index d75b0f7f3..83c740c5c 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -1053,7 +1053,7 @@ void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) std::string typeName(""); handler.serializeString("type", typeName); if(!typeName.empty()) - setType(VLC->creh->getCreature("core", typeName)); + setType(VLC->creh->getCreature(CModHandler::scopeMap(), typeName)); } } diff --git a/lib/CGameState.cpp b/lib/CGameState.cpp index 6d318de54..90c86c983 100644 --- a/lib/CGameState.cpp +++ b/lib/CGameState.cpp @@ -760,6 +760,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, bool allow initHeroes(); initStartingBonus(); initTowns(); + placeHeroesInTowns(); initMapObjects(); buildBonusSystemTree(); initVisitingAndGarrisonedHeroes(); @@ -1363,7 +1364,8 @@ void CGameState::placeStartingHeroes() { if(auto campaignBonus = scenarioOps->campState->getBonusForCurrentMap()) { - if(campaignBonus->type == CScenarioTravel::STravelBonus::HERO && playerColor == PlayerColor(campaignBonus->info1)) continue; + if(campaignBonus->type == CScenarioTravel::STravelBonus::HERO && playerColor == PlayerColor(campaignBonus->info1)) + continue; } } @@ -1861,6 +1863,37 @@ void CGameState::initMapObjects() map->calculateGuardingGreaturePositions(); //calculate once again when all the guards are placed and initialized } +void CGameState::placeHeroesInTowns() +{ + for(auto k=players.begin(); k!=players.end(); ++k) + { + if(k->first==PlayerColor::NEUTRAL) + continue; + + for(CGHeroInstance *h : k->second.heroes) + { + for(CGTownInstance *t : k->second.towns) + { + bool heroOnTownBlockableTile = t->blockingAt(h->visitablePos().x, h->visitablePos().y); + + // current hero position is at one of blocking tiles of current town + // assume that this hero should be visiting the town (H3M format quirk) and move hero to correct position + if (heroOnTownBlockableTile) + { + int3 townVisitablePos = t->visitablePos(); + int3 correctedPos = townVisitablePos + h->getVisitableOffset(); + + map->removeBlockVisTiles(h); + h->pos = correctedPos; + map->addBlockVisTiles(h); + + assert(t->visitableAt(h->visitablePos().x, h->visitablePos().y)); + } + } + } + } +} + void CGameState::initVisitingAndGarrisonedHeroes() { for(auto k=players.begin(); k!=players.end(); ++k) @@ -1873,17 +1906,10 @@ void CGameState::initVisitingAndGarrisonedHeroes() { for(CGTownInstance *t : k->second.towns) { - int3 vistile = t->visitablePos(); vistile.x++; //tile next to the entrance - if(vistile == h->pos || h->pos==t->visitablePos()) + if (t->visitableAt(h->visitablePos().x, h->visitablePos().y)) { + assert(t->visitingHero == nullptr); t->setVisitingHero(h); - if(h->pos == t->pos) //visiting hero placed in the editor has same pos as the town - we need to correct it - { - map->removeBlockVisTiles(h); - h->pos.x -= 1; - map->addBlockVisTiles(h); - } - break; } } } diff --git a/lib/CGameState.h b/lib/CGameState.h index 76a6dbd79..85e4b72c5 100644 --- a/lib/CGameState.h +++ b/lib/CGameState.h @@ -269,6 +269,7 @@ private: void placeStartingHero(PlayerColor playerColor, HeroTypeID heroTypeId, int3 townPos); void initStartingResources(); void initHeroes(); + void placeHeroesInTowns(); void giveCampaignBonusToHero(CGHeroInstance * hero); void initFogOfWar(); void initStartingBonus(); diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 78ffba2eb..72bc7ce37 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -348,7 +348,7 @@ CHeroHandler::CHeroHandler() loadTerrains(); for(const auto & terrain : VLC->terrainTypeHandler->terrains()) { - VLC->modh->identifiers.registerObject("core", "terrain", terrain.name, terrain.id); + VLC->modh->identifiers.registerObject(CModHandler::scopeBuiltin(), "terrain", terrain.name, terrain.id); } loadBallistics(); loadExperience(); @@ -868,7 +868,7 @@ std::vector CHeroHandler::loadLegacyData(size_t dataSize) void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { size_t index = objects.size(); - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name), index); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name), index); object->imageIndex = (si32)index + GameConstants::HERO_PORTRAIT_SHIFT; // 2 special frames + some extra portraits objects.push_back(object); @@ -878,7 +878,7 @@ void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNod void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name), index); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name), index); object->imageIndex = static_cast(index); assert(objects[index] == nullptr); // ensure that this id was not loaded before diff --git a/lib/CModHandler.cpp b/lib/CModHandler.cpp index aae7cffa1..a05f851f8 100644 --- a/lib/CModHandler.cpp +++ b/lib/CModHandler.cpp @@ -209,16 +209,15 @@ std::vector CIdentifierStorage::getPossibleIdent // called have not specified destination mod explicitly if (request.remoteScope.empty()) { - // FIXME: temporary, for queries from map loader allow access to any identifer - // should be changed to list of mods that are marked as required by current map - if (request.localScope == "map") + // special scope that should have access to all in-game objects + if (request.localScope == CModHandler::scopeGame()) { for (auto const & modName : VLC->modh->getActiveMods()) allowedScopes.insert(modName); } - // normally ID's from all required mods, own mod and virtual "core" mod are allowed - else if(request.localScope != "core" && !request.localScope.empty()) + // 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); @@ -229,15 +228,19 @@ std::vector CIdentifierStorage::getPossibleIdent } // all mods can access built-in mod - allowedScopes.insert("core"); + allowedScopes.insert(CModHandler::scopeBuiltin()); } else { //if destination mod was specified explicitly, restrict lookup to this mod - - if(request.remoteScope == "core" ) + if(request.remoteScope == CModHandler::scopeBuiltin() ) { - //"core" mod is an implicit dependency for all mods, allow access into it + //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 ) @@ -340,7 +343,7 @@ ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, std::string objec { for(auto & node : originalData) { - node.setMeta("core"); + node.setMeta(CModHandler::scopeBuiltin()); } } @@ -401,6 +404,9 @@ bool ContentTypeHandler::loadMod(std::string modName, bool validate) 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()); @@ -413,7 +419,7 @@ bool ContentTypeHandler::loadMod(std::string modName, bool validate) } else { - logMod->warn("no original data in loadMod(%s) at index %d", name, index); + logMod->trace("no original data in loadMod(%s) at index %d", name, index); } performValidate(data, name); handler->loadObject(modName, name, data, index); @@ -506,7 +512,7 @@ void CContentHandler::preloadData(CModInfo & mod) // print message in format [<8-symbols checksum>] logMod->info("\t\t[%08x]%s", mod.checksum, mod.name); - if (validate && mod.identifier != "core") + if (validate && mod.identifier != CModHandler::scopeBuiltin()) { if (!JsonUtils::validate(mod.config, "vcmi:mod", mod.identifier)) mod.validation = CModInfo::FAILED; @@ -698,13 +704,13 @@ CModHandler::CModHandler() : content(std::make_shared()) modules.MITHRIL = false; for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) { - identifiers.registerObject("core", "resource", GameConstants::RESOURCE_NAMES[i], i); + identifiers.registerObject(CModHandler::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); } for(int i=0; i CModHandler::getModList(std::string path) return foundMods; } +bool CModHandler::isScopeReserved(const TModID & scope) +{ + static const std::array reservedScopes = { + "core", "map", "game" + }; + + 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(std::string path, std::string parent, const JsonNode & modSettings, bool enableMods) { for(std::string modName : getModList(path)) @@ -916,6 +951,12 @@ void CModHandler::loadOneMod(std::string modName, std::string parent, const Json boost::to_lower(modName); std::string modFullName = parent.empty() ? modName : parent + '.' + modName; + if ( isScopeReserved(modFullName)) + { + logMod->error("Can not load mod %s - this name is reserved for internal use!", modFullName); + return; + } + if(CResourceHandler::get("initial")->existsResource(ResourceID(CModInfo::getModFile(modFullName)))) { CModInfo mod(modFullName, modSettings[modName], JsonNode(ResourceID(CModInfo::getModFile(modFullName)))); @@ -944,7 +985,7 @@ void CModHandler::loadMods(bool onlyEssential) loadMods("", "", modConfig["activeMods"], true); } - coreMod = CModInfo("core", modConfig["core"], JsonNode(ResourceID("config/gameConfig.json"))); + coreMod = CModInfo(CModHandler::scopeBuiltin(), modConfig[CModHandler::scopeBuiltin()], JsonNode(ResourceID("config/gameConfig.json"))); coreMod.name = "Original game files"; } @@ -991,7 +1032,7 @@ static ui32 calculateModChecksum(const std::string modName, ISimpleResourceLoade // second - add mod.json into checksum because filesystem does not contains this file // FIXME: remove workaround for core mod - if (modName != "core") + if (modName != CModHandler::scopeBuiltin()) { ResourceID modConfFile(CModInfo::getModFile(modName), EResType::TEXT); ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); @@ -1017,7 +1058,7 @@ void CModHandler::loadModFilesystems() { activeMods = validateAndSortDependencies(activeMods); - coreMod.updateChecksum(calculateModChecksum("core", CResourceHandler::get("core"))); + coreMod.updateChecksum(calculateModChecksum(CModHandler::scopeBuiltin(), CResourceHandler::get(CModHandler::scopeBuiltin()))); for(std::string & modName : activeMods) { @@ -1057,7 +1098,7 @@ void CModHandler::load() allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(modName))); } - // first - load virtual "core" mod that contains all data + // 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) @@ -1096,7 +1137,7 @@ void CModHandler::afterLoad(bool onlyEssential) modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); } - modSettings["core"] = coreMod.saveLocalData(); + modSettings[CModHandler::scopeBuiltin()] = coreMod.saveLocalData(); if(!onlyEssential) { diff --git a/lib/CModHandler.h b/lib/CModHandler.h index f152fdbd9..adf8cb752 100644 --- a/lib/CModHandler.h +++ b/lib/CModHandler.h @@ -283,7 +283,19 @@ class DLL_LINKAGE CModHandler void loadMods(std::string path, std::string parent, const JsonNode & modSettings, bool enableMods); void loadOneMod(std::string modName, std::string parent, const JsonNode & modSettings, bool enableMods); public: - + + /// 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: diff --git a/lib/CPathfinder.cpp b/lib/CPathfinder.cpp index 0d131f59e..e0c32210a 100644 --- a/lib/CPathfinder.cpp +++ b/lib/CPathfinder.cpp @@ -625,7 +625,7 @@ void LayerTransitionRule::process( else if(destination.node->accessible != CGPathNode::ACCESSIBLE) { /// Hero that fly can only land on accessible tiles - if(!destination.isGuardianTile && destination.nodeObject) + if(destination.nodeObject) destination.blocked = true; } diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 761444628..8a1a70c72 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -267,7 +267,7 @@ std::vector CSkillHandler::getDefaultAllowed() const si32 CSkillHandler::decodeSkill(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", "skill", identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "skill", identifier); if(rawId) return rawId.get(); else diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 1cb2cd553..6cda5f1e9 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -1010,7 +1010,7 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name), objects.size()); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name), objects.size()); objects.push_back(object); @@ -1049,7 +1049,7 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name), index); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name), index); if (objects.size() > index) assert(objects[index] == nullptr); // ensure that this id was not loaded before @@ -1083,7 +1083,7 @@ void CTownHandler::loadRandomFaction() static const ResourceID randomFactionPath("config/factions/random.json"); JsonNode randomFactionJson(randomFactionPath); - randomFactionJson.setMeta("core", true); + randomFactionJson.setMeta(CModHandler::scopeBuiltin(), true); loadBuildings(randomTown, randomFactionJson["random"]["town"]["buildings"]); } diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index 10bab6aee..38c00d67e 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -62,7 +62,7 @@ namespace GameConstants si32 HeroTypeID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", "hero", identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", identifier); if(rawId) return rawId.get(); else @@ -86,7 +86,7 @@ const Artifact * ArtifactID::toArtifact(const ArtifactService * service) const si32 ArtifactID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", "artifact", identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "artifact", identifier); if(rawId) return rawId.get(); else @@ -110,7 +110,7 @@ const Creature * CreatureID::toCreature(const CreatureService * creatures) const si32 CreatureID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", "creature", identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", identifier); if(rawId) return rawId.get(); else @@ -139,7 +139,7 @@ const spells::Spell * SpellID::toSpell(const spells::Service * service) const si32 SpellID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", "spell", identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "spell", identifier); if(rawId) return rawId.get(); else @@ -201,7 +201,7 @@ const FactionID FactionID::NEUTRAL = FactionID(9); si32 FactionID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", "faction", identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "faction", identifier); if(rawId) return rawId.get(); else @@ -289,7 +289,7 @@ const BattleFieldInfo * BattleField::getInfo() const BattleField BattleField::fromString(std::string identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", "battlefield", identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "battlefield", identifier); if(rawId) return BattleField(rawId.get()); @@ -309,7 +309,7 @@ Obstacle::operator std::string() const Obstacle Obstacle::fromString(std::string identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", "obstacle", identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "obstacle", identifier); if(rawId) return Obstacle(rawId.get()); diff --git a/lib/IHandlerBase.cpp b/lib/IHandlerBase.cpp index fe35ed850..a91e5430f 100644 --- a/lib/IHandlerBase.cpp +++ b/lib/IHandlerBase.cpp @@ -14,6 +14,10 @@ VCMI_LIB_NAMESPACE_BEGIN +std::string IHandlerBase::getScopeBuiltin() const +{ + return CModHandler::scopeBuiltin(); +} void IHandlerBase::registerObject(std::string scope, std::string type_name, std::string name, si32 index) { diff --git a/lib/IHandlerBase.h b/lib/IHandlerBase.h index 04aa4d0e7..d47bbd820 100644 --- a/lib/IHandlerBase.h +++ b/lib/IHandlerBase.h @@ -21,6 +21,8 @@ class Entity; class DLL_LINKAGE IHandlerBase { protected: + std::string getScopeBuiltin() const; + /// Calls modhandler. Mostly needed to avoid large number of includes in headers void registerObject(std::string scope, std::string type_name, std::string name, si32 index); std::string normalizeIdentifier(const std::string & scope, const std::string & remoteScope, const std::string & identifier) const; @@ -92,7 +94,7 @@ public: void loadObject(std::string scope, std::string name, const JsonNode & data) override { - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name), objects.size()); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, getScopeBuiltin(), name), objects.size()); objects.push_back(object); @@ -102,7 +104,7 @@ public: void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override { - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name), index); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, getScopeBuiltin(), name), index); assert(objects[index] == nullptr); // ensure that this id was not loaded before objects[index] = object; diff --git a/lib/JsonDetail.cpp b/lib/JsonDetail.cpp index 3d0a6c47e..07ccb86ea 100644 --- a/lib/JsonDetail.cpp +++ b/lib/JsonDetail.cpp @@ -999,7 +999,7 @@ namespace bool testFilePresence(std::string scope, ResourceID resource) { std::set allowedScopes; - if(scope != "core" && !scope.empty()) // all real mods may have dependencies + if(scope != CModHandler::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; @@ -1008,7 +1008,7 @@ namespace if(!found) return false; - allowedScopes.insert("core"); // all mods can use H3 files + allowedScopes.insert(CModHandler::scopeBuiltin()); // all mods can use H3 files } allowedScopes.insert(scope); // mods can use their own files diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 57d891850..b17c66333 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -703,13 +703,18 @@ DLL_LINKAGE void GiveHero::applyGs(CGameState *gs) //bonus system h->detachFrom(gs->globalEffects); h->attachTo(*gs->getPlayerState(player)); - h->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, h->type->heroClass->getIndex())->getTemplates().front(); + auto oldOffset = h->getVisitableOffset(); gs->map->removeBlockVisTiles(h,true); + h->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, h->type->heroClass->getIndex())->getTemplates().front(); + auto newOffset = h->getVisitableOffset(); + h->setOwner(player); h->movement = h->maxMovePoints(true); + h->pos = h->pos - oldOffset + newOffset; gs->map->heroesOnMap.push_back(h); gs->getPlayerState(h->getOwner())->heroes.push_back(h); + gs->map->addBlockVisTiles(h); h->inTownGarrison = false; } diff --git a/lib/ScriptHandler.cpp b/lib/ScriptHandler.cpp index b64b0de2b..a066e2d0a 100644 --- a/lib/ScriptHandler.cpp +++ b/lib/ScriptHandler.cpp @@ -242,7 +242,7 @@ ScriptPtr ScriptHandler::loadFromJson(vstd::CLoggerBase * logger, const std::str void ScriptHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { - auto object = loadFromJson(logMod, scope, data, normalizeIdentifier(scope, "core", name)); + auto object = loadFromJson(logMod, scope, data, normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name)); objects[object->identifier] = object; } diff --git a/lib/Terrain.cpp b/lib/Terrain.cpp index fff98de34..599ab40ea 100644 --- a/lib/Terrain.cpp +++ b/lib/Terrain.cpp @@ -22,7 +22,7 @@ VCMI_LIB_NAMESPACE_BEGIN TerrainTypeHandler::TerrainTypeHandler() { auto allConfigs = VLC->modh->getActiveMods(); - allConfigs.insert(allConfigs.begin(), "core"); + allConfigs.insert(allConfigs.begin(), CModHandler::scopeBuiltin()); initRivers(allConfigs); recreateRiverMaps(); diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index 5796c4bc9..ea10bb7cb 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -20,6 +20,7 @@ #include "../GameConstants.h" #include "../VCMIDirs.h" #include "../CStopWatch.h" +#include "../CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -207,7 +208,7 @@ void CResourceHandler::load(const std::string &fsConfigURI, bool extractArchives const JsonNode fsConfig((char*)fsConfigData.first.get(), fsConfigData.second); - addFilesystem("data", "core", createFileSystem("", fsConfig["filesystem"], extractArchives)); + addFilesystem("data", CModHandler::scopeBuiltin(), createFileSystem("", fsConfig["filesystem"], extractArchives)); } void CResourceHandler::addFilesystem(const std::string & parent, const std::string & identifier, ISimpleResourceLoader * loader) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index b14473ddd..e876b1cdc 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1413,7 +1413,7 @@ void CGHeroInstance::setHeroTypeName(const std::string & identifier) { if(ID == Obj::HERO || ID == Obj::PRISON) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", "hero", identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", identifier); if(rawId) subID = rawId.get(); @@ -1434,7 +1434,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) handler.serializeString("biography", biography); handler.serializeInt("experience", exp, 0); - if (!handler.saving) + if(!handler.saving && exp != 0xffffffff) //do not gain levels if experience is not initialized { while (gainsLevel()) { diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 7fd19f76e..bb6a0bb8c 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1466,7 +1466,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) { auto decodeBuilding = [this](const std::string & identifier) -> si32 { - auto rawId = VLC->modh->identifiers.getIdentifier("core", getTown()->getBuildingScope(), identifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), getTown()->getBuildingScope(), identifier); if(rawId) return rawId.get(); diff --git a/lib/mapObjects/CObjectClassesHandler.cpp b/lib/mapObjects/CObjectClassesHandler.cpp index f6d5dbcaf..e8580e6b9 100644 --- a/lib/mapObjects/CObjectClassesHandler.cpp +++ b/lib/mapObjects/CObjectClassesHandler.cpp @@ -170,7 +170,7 @@ void CObjectClassesHandler::loadObjectEntry(const std::string & identifier, cons logGlobal->error("Handler with name %s was not found!", obj->handlerName); return; } - const auto convertedId = VLC->modh->normalizeIdentifier(entry.meta, "core", identifier); + const auto convertedId = VLC->modh->normalizeIdentifier(entry.meta, CModHandler::scopeBuiltin(), identifier); const auto & entryIndex = entry["index"]; bool useSelectNextID = !isSubobject || entryIndex.isNull(); @@ -259,14 +259,14 @@ CObjectClassesHandler::ObjectContainter * CObjectClassesHandler::loadFromJson(co void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name)); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name)); objects[object->id] = object; VLC->modh->identifiers.registerObject(scope, "object", name, object->id); } void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { - auto object = loadFromJson(scope, data, normalizeIdentifier(scope, "core", name)); + auto object = loadFromJson(scope, data, normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name)); assert(objects[(si32)index] == nullptr); // ensure that this id was not loaded before objects[(si32)index] = object; VLC->modh->identifiers.registerObject(scope, "object", name, object->id); @@ -308,8 +308,9 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) if (objects.at(type)->subObjects.count(subtype)) return objects.at(type)->subObjects.at(subtype); } - logGlobal->error("Failed to find object of type %d:%d", type, subtype); - throw std::runtime_error("Object type handler not found"); + std::string errorString = "Failed to find object of type " + std::to_string(type) + "::" + std::to_string(subtype); + logGlobal->error(errorString); + throw std::runtime_error(errorString); } TObjectTypeHandler CObjectClassesHandler::getHandlerFor(std::string scope, std::string type, std::string subtype) const @@ -325,8 +326,9 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(std::string scope, std:: return object->subObjects.at(subId); } } - logGlobal->error("Failed to find object of type %s::%s", type, subtype); - throw std::runtime_error("Object type handler not found"); + std::string errorString = "Failed to find object of type " + type + "::" + subtype; + logGlobal->error(errorString); + throw std::runtime_error(errorString); } TObjectTypeHandler CObjectClassesHandler::getHandlerFor(CompoundMapObjectID compoundIdentifier) const diff --git a/lib/mapObjects/CObjectHandler.cpp b/lib/mapObjects/CObjectHandler.cpp index 2df272495..d6f27bbff 100644 --- a/lib/mapObjects/CObjectHandler.cpp +++ b/lib/mapObjects/CObjectHandler.cpp @@ -195,7 +195,9 @@ std::set CGObjectInstance::getBlockedOffsets() const void CGObjectInstance::setType(si32 ID, si32 subID) { - const TerrainTile &tile = cb->gameState()->map->getTile(visitablePos()); + auto position = visitablePos(); + auto oldOffset = getVisitableOffset(); + auto &tile = cb->gameState()->map->getTile(position); //recalculate blockvis tiles - new appearance might have different blockmap than before cb->gameState()->map->removeBlockVisTiles(this, true); @@ -206,14 +208,23 @@ void CGObjectInstance::setType(si32 ID, si32 subID) return; } if(!handler->getTemplates(tile.terType->id).empty()) + { appearance = handler->getTemplates(tile.terType->id)[0]; + } else + { + logGlobal->warn("Object %d:%d at %s has no templates suitable for terrain %s", ID, subID, visitablePos().toString(), tile.terType->name); appearance = handler->getTemplates()[0]; // get at least some appearance since alternative is crash + } if(this->ID == Obj::PRISON && ID == Obj::HERO) { - //adjust for the prison offset - pos = visitablePos(); + auto newOffset = getVisitableOffset(); + // FIXME: potentially unused code - setType is NOT called when releasing hero from prison + // instead, appearance update & pos adjustment occurs in GiveHero::applyGs + + // adjust position since hero and prison may have different visitable offset + pos = pos - oldOffset + newOffset; } this->ID = Obj(ID); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 05c07379b..837841715 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -1028,7 +1028,7 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) if(doRequest) { - auto rawId = VLC->modh->identifiers.getIdentifier("core", fullIdentifier, false); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), fullIdentifier, false); if(rawId) { diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 8e78fc8e3..0db513b8e 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -970,22 +970,22 @@ void CGVisitableOPH::initObj(CRandomGenerator & rand) break; case Obj::LIBRARY_OF_ENLIGHTENMENT: { + selectMode = SELECT_FIRST; onVisited.addTxt(MetaString::ADVOB_TXT, 67); onEmpty.addTxt(MetaString::ADVOB_TXT, 68); - // Don't like this one but don't see any easier approach CVisitInfo visit; visit.reward.primary[PrimarySkill::ATTACK] = 2; visit.reward.primary[PrimarySkill::DEFENSE] = 2; visit.reward.primary[PrimarySkill::KNOWLEDGE] = 2; visit.reward.primary[PrimarySkill::SPELL_POWER] = 2; + visit.message.addTxt(MetaString::ADVOB_TXT, 66); static_assert(SecSkillLevel::LEVELS_SIZE == 4, "Behavior of Library of Enlignment may not be correct"); for (int i=0; imodh->identifiers.getIdentifier("core", "primSkill", json["rewardPrimSkill"].String()); + auto raw = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "primSkill", json["rewardPrimSkill"].String()); if(raw) { bonusType = PRIM_SKILL; @@ -1813,7 +1813,7 @@ void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) } else if(!json["rewardSkill"].String().empty()) { - auto raw = VLC->modh->identifiers.getIdentifier("core", "skill", json["rewardSkill"].String()); + auto raw = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "skill", json["rewardSkill"].String()); if(raw) { bonusType = SECONDARY_SKILL; @@ -1822,7 +1822,7 @@ void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) } else if(!json["rewardSpell"].String().empty()) { - auto raw = VLC->modh->identifiers.getIdentifier("core", "spell", json["rewardSpell"].String()); + auto raw = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "spell", json["rewardSpell"].String()); if(raw) { bonusType = SPELL; diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index 9caec2379..d91536542 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -287,7 +287,15 @@ void ObjectTemplate::readJson(const JsonNode &node, const bool withTerrain) if(withTerrain && !node["allowedTerrains"].isNull()) { for(auto& entry : node["allowedTerrains"].Vector()) - allowedTerrains.insert(VLC->terrainTypeHandler->getInfoByName(entry.String())->id); + { + try { + allowedTerrains.insert(VLC->terrainTypeHandler->getInfoByName(entry.String())->id); + } + catch (const std::out_of_range & ) + { + logGlobal->warn("Failed to find terrain '%s' for object '%s'", entry.String(), animationFile); + } + } } else { @@ -300,7 +308,7 @@ void ObjectTemplate::readJson(const JsonNode &node, const bool withTerrain) } if(withTerrain && allowedTerrains.empty()) - logGlobal->warn("Loaded template without allowed terrains!"); + logGlobal->warn("Loaded template %s without allowed terrains!", animationFile); auto charToTile = [&](const char & ch) -> ui8 { diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index d5f3fee82..4d55bc0b7 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -256,6 +256,8 @@ CMap::CMap() CMap::~CMap() { + getEditManager()->getUndoManager().clearAll(); + if(terrain) { for(int z = 0; z < levels(); z++) diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 2a3e78a81..0d58beff5 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -215,7 +215,7 @@ namespace TriggeredEventsDetail event.metaType = decodeMetaclass(metaTypeName); - auto type = VLC->modh->identifiers.getIdentifier("core", fullIdentifier, false); + auto type = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), fullIdentifier, false); if(type) event.objectType = type.get(); @@ -1121,7 +1121,7 @@ void CMapLoaderJson::MapObjectLoader::construct() return; } - auto handler = VLC->objtypeh->getHandlerFor( "map", typeName, subtypeName); + auto handler = VLC->objtypeh->getHandlerFor( CModHandler::scopeMap(), typeName, subtypeName); auto appearance = new ObjectTemplate; @@ -1158,7 +1158,7 @@ void CMapLoaderJson::MapObjectLoader::configure() if(art->ID == Obj::SPELL_SCROLL) { auto spellIdentifier = configuration["options"]["spell"].String(); - auto rawId = VLC->modh->identifiers.getIdentifier("core", "spell", spellIdentifier); + auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "spell", spellIdentifier); if(rawId) spellID = rawId.get(); else diff --git a/lib/rmg/CRmgTemplateStorage.cpp b/lib/rmg/CRmgTemplateStorage.cpp index ee0564488..699652830 100644 --- a/lib/rmg/CRmgTemplateStorage.cpp +++ b/lib/rmg/CRmgTemplateStorage.cpp @@ -14,6 +14,7 @@ #include "CRmgTemplate.h" #include "../serializer/JsonDeserializer.h" +#include "../CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -30,7 +31,7 @@ void CRmgTemplateStorage::loadObject(std::string scope, std::string name, const try { JsonDeserializer handler(nullptr, data); - auto fullKey = normalizeIdentifier(scope, "core", name); //actually it's not used + auto fullKey = normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name); //actually it's not used templates[fullKey].setId(name); templates[fullKey].serializeJson(handler); templates[fullKey].validate(); diff --git a/lib/rmg/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index 431b43ee2..33c9639dc 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -529,7 +529,12 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) } for (auto zone : zones) + { + if(zone.second->area().empty()) + throw rmgException("Empty zone is generated, probably RMG template is inappropriate for map size"); + moveZoneToCenterOfMass(zone.second); + } //assign actual tiles to each zone using nonlinear norm for fine edges diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index e6dfa09a2..9cf572727 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -763,7 +763,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & { if(counteredSpell.second.Bool()) { - VLC->modh->identifiers.requestIdentifier(json.meta, counteredSpell.first, [=](si32 id) + VLC->modh->identifiers.requestIdentifier(counteredSpell.second.meta, counteredSpell.first, [=](si32 id) { spell->counteredSpells.push_back(SpellID(id)); }); diff --git a/mapeditor/icons/edit-redo.png b/mapeditor/icons/edit-redo.png new file mode 100644 index 000000000..3eb7b05c8 Binary files /dev/null and b/mapeditor/icons/edit-redo.png differ diff --git a/mapeditor/icons/edit-undo.png b/mapeditor/icons/edit-undo.png new file mode 100644 index 000000000..61b2ce9a5 Binary files /dev/null and b/mapeditor/icons/edit-undo.png differ diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index b5a26a41f..e575f660a 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -555,6 +555,9 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari if(key == "Name") o->name = value.toString().toStdString(); + if(key == "Experience") + o->exp = value.toInt(); + if(key == "Hero type") { for(auto t : VLC->heroh->objects) diff --git a/mapeditor/inspector/townbulidingswidget.cpp b/mapeditor/inspector/townbulidingswidget.cpp index 04dbfdbdd..6164c92cd 100644 --- a/mapeditor/inspector/townbulidingswidget.cpp +++ b/mapeditor/inspector/townbulidingswidget.cpp @@ -172,11 +172,22 @@ void TownBulidingsWidget::addBuildings(const CTown & ctown) std::set TownBulidingsWidget::getBuildingsFromModel(int modelColumn, Qt::CheckState checkState) { std::set result; - for(int i = 0; i < model.rowCount(); ++i) + std::vector stack; + stack.push_back(QModelIndex()); + while(!stack.empty()) { - if(auto * item = model.item(i, modelColumn)) - if(item->checkState() == checkState) - result.emplace(item->data(Qt::UserRole).toInt()); + auto pindex = stack.back(); + stack.pop_back(); + for(int i = 0; i < model.rowCount(pindex); ++i) + { + QModelIndex index = model.index(i, modelColumn, pindex); + if(auto * item = model.itemFromIndex(index)) + if(item->checkState() == checkState) + result.emplace(item->data(Qt::UserRole).toInt()); + index = model.index(i, 0, pindex); //children are linked to first column of the model + if(model.hasChildren(index)) + stack.push_back(index); + } } return result; diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 3acf2d678..c119bee1f 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -120,6 +120,10 @@ MainWindow::MainWindow(QWidget* parent) : ui(new Ui::MainWindow), controller(this) { + // Set current working dir to executable folder. + // This is important on Mac for relative paths to work inside DMG. + QDir::setCurrent(QApplication::applicationDirPath()); + for(auto & string : VCMIDirs::get().dataPaths()) QDir::addSearchPath("icons", pathToQString(string / "mapeditor" / "icons")); QDir::addSearchPath("icons", pathToQString(VCMIDirs::get().userDataPath() / "mapeditor" / "icons")); @@ -128,10 +132,6 @@ MainWindow::MainWindow(QWidget* parent) : loadUserSettings(); //For example window size setTitle(); - // Set current working dir to executable folder. - // This is important on Mac for relative paths to work inside DMG. - QDir::setCurrent(QApplication::applicationDirPath()); - ExtractionOptions extractionOptions; parseCommandLine(extractionOptions); @@ -550,7 +550,7 @@ void MainWindow::loadObjectsTree() //model objectsModel.setHorizontalHeaderLabels(QStringList() << tr("Type")); - objectBrowser = new ObjectBrowser(this); + objectBrowser = new ObjectBrowserProxyModel(this); objectBrowser->setSourceModel(&objectsModel); objectBrowser->setDynamicSortFilter(false); objectBrowser->setRecursiveFilteringEnabled(true); @@ -853,12 +853,11 @@ void MainWindow::on_toolErase_clicked() ui->tabWidget->setCurrentIndex(0); } -void MainWindow::preparePreview(const QModelIndex &index, bool createNew) +void MainWindow::preparePreview(const QModelIndex &index) { scenePreview->clear(); auto data = objectsModel.itemFromIndex(objectBrowser->mapToSource(index))->data().toJsonObject(); - if(!data.empty()) { auto preview = data["preview"]; @@ -866,29 +865,12 @@ void MainWindow::preparePreview(const QModelIndex &index, bool createNew) { QPixmap objPreview = pixmapFromJson(preview); scenePreview->addPixmap(objPreview); - - auto objId = data["id"].toInt(); - auto objSubId = data["subid"].toInt(); - auto templateId = data["template"].toInt(); - - if(controller.discardObject(mapLevel) || createNew) - { - auto factory = VLC->objtypeh->getHandlerFor(objId, objSubId); - auto templ = factory->getTemplates()[templateId]; - controller.createObject(mapLevel, factory->create(templ)); - } } } } void MainWindow::treeViewSelected(const QModelIndex & index, const QModelIndex & deselected) -{ - preparePreview(index, false); -} - - -void MainWindow::on_treeView_activated(const QModelIndex &index) { ui->toolBrush->setChecked(false); ui->toolBrush2->setChecked(false); @@ -896,11 +878,10 @@ void MainWindow::on_treeView_activated(const QModelIndex &index) ui->toolArea->setChecked(false); ui->toolLasso->setChecked(false); ui->mapView->selectionTool = MapView::SelectionTool::None; - - preparePreview(index, true); + + preparePreview(index); } - void MainWindow::on_terrainFilterCombo_currentTextChanged(const QString &arg1) { if(!objectBrowser) diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index dc6acdd38..3464c45b2 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -8,6 +8,7 @@ #include "resourceExtractor/ResourceConverter.h" class ObjectBrowser; +class ObjectBrowserProxyModel; VCMI_LIB_NAMESPACE_BEGIN class CMap; @@ -77,8 +78,6 @@ private slots: void on_toolErase_clicked(); - void on_treeView_activated(const QModelIndex &index); - void on_terrainFilterCombo_currentTextChanged(const QString &arg1); void on_filter_textChanged(const QString &arg1); @@ -116,7 +115,7 @@ public slots: void displayStatus(const QString& message, int timeout = 2000); private: - void preparePreview(const QModelIndex &index, bool createNew); + void preparePreview(const QModelIndex & index); void addGroupIntoCatalog(const std::string & groupName, bool staticOnly); void addGroupIntoCatalog(const std::string & groupName, bool useCustomName, bool staticOnly, int ID); @@ -136,7 +135,7 @@ private: private: Ui::MainWindow * ui; - ObjectBrowser * objectBrowser = nullptr; + ObjectBrowserProxyModel * objectBrowser = nullptr; QGraphicsScene * scenePreview; QString filename; diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index f23450ca9..6da92787e 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -123,6 +123,9 @@ + + + @@ -300,7 +303,7 @@ - + 0 @@ -316,8 +319,11 @@ QAbstractItemView::NoEditTriggers + + false + - QAbstractItemView::NoDragDrop + QAbstractItemView::DragOnly QAbstractItemView::SelectItems @@ -1010,6 +1016,9 @@ + + false + Players settings @@ -1018,6 +1027,10 @@ false + + + icons:edit-undo.pngicons:edit-undo.png + Undo @@ -1035,6 +1048,10 @@ false + + + icons:edit-redo.pngicons:edit-redo.png + Redo @@ -1192,6 +1209,11 @@ QGraphicsView
mapview.h
+ + ObjectBrowser + QTreeView +
objectbrowser.h
+
diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index c59859db8..aa3a0c92b 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -128,10 +128,15 @@ void MapController::repairMap() assert(type->heroClass); //TODO: find a way to get proper type name if(obj->ID == Obj::HERO) + { nih->typeName = "hero"; + nih->subTypeName = type->heroClass->identifier; + } if(obj->ID == Obj::PRISON) + { nih->typeName = "prison"; - nih->subTypeName = type->heroClass->identifier; + nih->subTypeName = "prison"; + } nih->type = type; if(nih->name.empty()) @@ -214,6 +219,7 @@ void MapController::setMap(std::unique_ptr cmap) main->enableRedo(allowRedo); } ); + _map->getEditManager()->getUndoManager().clearAll(); } void MapController::sceneForceUpdate() diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index d9460d7a4..0cd4b9a5a 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -13,6 +13,7 @@ #include "mainwindow.h" #include #include "mapcontroller.h" +#include "../lib/mapObjects/CObjectClassesHandler.h" MinimapView::MinimapView(QWidget * parent): QGraphicsView(parent) @@ -64,12 +65,10 @@ MapView::MapView(QWidget * parent): void MapView::cameraChanged(const QPointF & pos) { - //ui->mapView->translate(pos.x(), pos.y()); horizontalScrollBar()->setValue(pos.x()); verticalScrollBar()->setValue(pos.y()); } - void MapView::setController(MapController * ctrl) { controller = ctrl; @@ -85,7 +84,12 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) auto pos = mapToScene(mouseEvent->pos()); //TODO: do we need to check size? int3 tile(pos.x() / 32, pos.y() / 32, sc->level); - + + //if scene will be scrolled without mouse movement, selection, object moving and rubber band will not be updated + //to change this behavior, all this logic should be placed in viewportEvent + if(rubberBand) + rubberBand->setGeometry(QRect(mapFromScene(mouseStart), mouseEvent->pos()).normalized()); + if(tile == tilePrev) //do not redraw return; @@ -161,13 +165,7 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) if(sh.x || sh.y) { - if(sc->selectionObjectsView.newObject) - { - sc->selectionObjectsView.shift = QPoint(tile.x, tile.y); - sc->selectionObjectsView.selectObject(sc->selectionObjectsView.newObject); - sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::MOVEMENT; - } - else if(mouseEvent->buttons() & Qt::LeftButton) + if(!sc->selectionObjectsView.newObject && (mouseEvent->buttons() & Qt::LeftButton)) { if(sc->selectionObjectsView.selectionMode == SelectionObjectsLayer::SELECTION) { @@ -296,6 +294,11 @@ void MapView::mousePressEvent(QMouseEvent *event) { sc->selectionObjectsView.clear(); sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::SELECTION; + + if(!rubberBand) + rubberBand = new QRubberBand(QRubberBand::Rectangle, this); + rubberBand->setGeometry(QRect(mapFromScene(mouseStart), QSize())); + rubberBand->show(); } } sc->selectionObjectsView.shift = QPoint(0, 0); @@ -314,6 +317,9 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) auto * sc = static_cast(scene()); if(!sc || !controller->map()) return; + + if(rubberBand) + rubberBand->hide(); switch(selectionTool) { @@ -324,19 +330,7 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) bool tab = false; if(sc->selectionObjectsView.selectionMode == SelectionObjectsLayer::MOVEMENT) { - if(sc->selectionObjectsView.newObject) - { - QString errorMsg; - if(controller->canPlaceObject(sc->level, sc->selectionObjectsView.newObject, errorMsg)) - controller->commitObjectCreate(sc->level); - else - { - QMessageBox::information(this, "Can't place object", errorMsg); - break; - } - } - else - controller->commitObjectShift(sc->level); + controller->commitObjectShift(sc->level); } else { @@ -355,11 +349,103 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) } } +void MapView::dragEnterEvent(QDragEnterEvent * event) +{ + if(!controller || !controller->map()) + return; + + auto * sc = static_cast(scene()); + if(!sc) + return; + + if(event->mimeData()->hasImage()) + { + logGlobal->info("Drag'n'drop: dispatching object"); + QVariant vdata = event->mimeData()->imageData(); + auto data = vdata.toJsonObject(); + if(!data.empty()) + { + auto preview = data["preview"]; + if(preview != QJsonValue::Undefined) + { + auto objId = data["id"].toInt(); + auto objSubId = data["subid"].toInt(); + auto templateId = data["template"].toInt(); + auto factory = VLC->objtypeh->getHandlerFor(objId, objSubId); + auto templ = factory->getTemplates()[templateId]; + controller->discardObject(sc->level); + controller->createObject(sc->level, factory->create(templ)); + } + } + + event->acceptProposedAction(); + } +} + +void MapView::dropEvent(QDropEvent * event) +{ + if(!controller || !controller->map()) + return; + + auto * sc = static_cast(scene()); + if(!sc) + return; + + if(sc->selectionObjectsView.newObject) + { + QString errorMsg; + if(controller->canPlaceObject(sc->level, sc->selectionObjectsView.newObject, errorMsg)) + { + controller->commitObjectCreate(sc->level); + } + else + { + controller->discardObject(sc->level); + QMessageBox::information(this, "Can't place object", errorMsg); + } + } + + event->acceptProposedAction(); +} + +void MapView::dragMoveEvent(QDragMoveEvent * event) +{ + auto * sc = static_cast(scene()); + if(!sc) + return; + + auto rect = event->answerRect(); + auto pos = mapToScene(rect.bottomRight()); //TODO: do we need to check size? + int3 tile(pos.x() / 32 + 1, pos.y() / 32 + 1, sc->level); + + if(sc->selectionObjectsView.newObject) + { + sc->selectionObjectsView.shift = QPoint(tile.x, tile.y); + sc->selectionObjectsView.selectObject(sc->selectionObjectsView.newObject); + sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::MOVEMENT; + sc->selectionObjectsView.draw(); + } + + event->acceptProposedAction(); +} + +void MapView::dragLeaveEvent(QDragLeaveEvent * event) +{ + if(!controller || !controller->map()) + return; + + auto * sc = static_cast(scene()); + if(!sc) + return; + + controller->discardObject(sc->level); +} + + bool MapView::viewportEvent(QEvent *event) { if(auto * sc = static_cast(scene())) { - //auto rect = sceneRect(); auto rect = mapToScene(viewport()->geometry()).boundingRect(); controller->miniScene(sc->level)->viewport.setViewport(rect.x() / 32, rect.y() / 32, rect.width() / 32, rect.height() / 32); } diff --git a/mapeditor/mapview.h b/mapeditor/mapview.h index cf4907a6e..009f40ac6 100644 --- a/mapeditor/mapview.h +++ b/mapeditor/mapview.h @@ -101,6 +101,10 @@ public slots: void mouseMoveEvent(QMouseEvent * mouseEvent) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; + void dragEnterEvent(QDragEnterEvent * event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dropEvent(QDropEvent * event) override; void cameraChanged(const QPointF & pos); @@ -113,6 +117,7 @@ protected: private: MapController * controller = nullptr; + QRubberBand * rubberBand = nullptr; QPointF mouseStart; int3 tileStart; int3 tilePrev; @@ -130,7 +135,7 @@ public: public slots: void mouseMoveEvent(QMouseEvent * mouseEvent) override; - void mousePressEvent(QMouseEvent* event) override; + void mousePressEvent(QMouseEvent * event) override; signals: void cameraPositionChanged(const QPointF & newPosition); diff --git a/mapeditor/objectbrowser.cpp b/mapeditor/objectbrowser.cpp index 416b8abfa..70d629208 100644 --- a/mapeditor/objectbrowser.cpp +++ b/mapeditor/objectbrowser.cpp @@ -12,12 +12,12 @@ #include "objectbrowser.h" #include "../lib/mapObjects/CObjectClassesHandler.h" -ObjectBrowser::ObjectBrowser(QObject *parent) +ObjectBrowserProxyModel::ObjectBrowserProxyModel(QObject *parent) : QSortFilterProxyModel{parent}, terrain(Terrain::ANY_TERRAIN) { } -bool ObjectBrowser::filterAcceptsRow(int source_row, const QModelIndex & source_parent) const +bool ObjectBrowserProxyModel::filterAcceptsRow(int source_row, const QModelIndex & source_parent) const { bool result = QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); @@ -57,7 +57,7 @@ bool ObjectBrowser::filterAcceptsRow(int source_row, const QModelIndex & source_ return result; } -bool ObjectBrowser::filterAcceptsRowText(int source_row, const QModelIndex &source_parent) const +bool ObjectBrowserProxyModel::filterAcceptsRowText(int source_row, const QModelIndex &source_parent) const { if(source_parent.isValid()) { @@ -76,3 +76,52 @@ bool ObjectBrowser::filterAcceptsRowText(int source_row, const QModelIndex &sour return (filter.isNull() || filter.isEmpty() || item->text().contains(filter, Qt::CaseInsensitive)); } +Qt::ItemFlags ObjectBrowserProxyModel::flags(const QModelIndex & index) const +{ + Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index); + + if (index.isValid()) + return Qt::ItemIsDragEnabled | defaultFlags; + + return defaultFlags; +} + +QMimeData * ObjectBrowserProxyModel::mimeData(const QModelIndexList & indexes) const +{ + assert(indexes.size() == 1); + + auto * standardModel = qobject_cast(sourceModel()); + assert(standardModel); + + QModelIndex index = indexes.front(); + + if(!index.isValid()) + return nullptr; + + QMimeData * mimeData = new QMimeData; + mimeData->setImageData(standardModel->itemFromIndex(mapToSource(index))->data()); + return mimeData; +} + +ObjectBrowser::ObjectBrowser(QWidget * parent): + QTreeView(parent) +{ + setDropIndicatorShown(false); +} + +void ObjectBrowser::startDrag(Qt::DropActions supportedActions) +{ + logGlobal->info("Drag'n'drop: Start dragging object from ObjectBrowser"); + QDrag *drag = new QDrag(this); + auto indexes = selectedIndexes(); + if(indexes.isEmpty()) + return; + + QMimeData * mimeData = model()->mimeData(indexes); + if(!mimeData) + return; + + drag->setMimeData(mimeData); + + Qt::DropAction dropAction = drag->exec(supportedActions); +} diff --git a/mapeditor/objectbrowser.h b/mapeditor/objectbrowser.h index 6c700ae62..2c5433ad2 100644 --- a/mapeditor/objectbrowser.h +++ b/mapeditor/objectbrowser.h @@ -13,10 +13,14 @@ #include #include "../lib/Terrain.h" -class ObjectBrowser : public QSortFilterProxyModel +class ObjectBrowserProxyModel : public QSortFilterProxyModel { public: - explicit ObjectBrowser(QObject *parent = nullptr); + explicit ObjectBrowserProxyModel(QObject *parent = nullptr); + + Qt::ItemFlags flags(const QModelIndex &index) const override; + + QMimeData * mimeData(const QModelIndexList & indexes) const override; TerrainId terrain; QString filter; @@ -25,3 +29,12 @@ protected: bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override; bool filterAcceptsRowText(int source_row, const QModelIndex &source_parent) const; }; + +class ObjectBrowser : public QTreeView +{ +public: + ObjectBrowser(QWidget * parent); + +protected: + void startDrag(Qt::DropActions supportedActions) override; +}; diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index b69d56a52..c5b4a582f 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -401,7 +401,7 @@ void SelectionObjectsLayer::draw() //show translation if(selectionMode == SelectionMode::MOVEMENT && (shift.x() || shift.y())) { - painter.setOpacity(0.5); + painter.setOpacity(0.7); auto newPos = QPoint(obj->getPosition().x, obj->getPosition().y) + shift; handler->drawObjectAt(painter, obj, newPos.x(), newPos.y()); } @@ -471,8 +471,11 @@ void SelectionObjectsLayer::selectObjects(int x1, int y1, int x2, int y2) { for(int i = x1; i < x2; ++i) { - for(auto & o : handler->getObjects(i, j, scene->level)) - selectObject(o.obj, false); //do not inform about each object added + 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 + } } } onSelection(); diff --git a/mapeditor/scenelayer.h b/mapeditor/scenelayer.h index 0e9b84d95..f796701bc 100644 --- a/mapeditor/scenelayer.h +++ b/mapeditor/scenelayer.h @@ -148,7 +148,6 @@ public: void deselectObject(CGObjectInstance *); bool isSelected(const CGObjectInstance *) const; std::set getSelection() const; - void moveSelection(int x, int y); void clear(); QPoint shift; diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp index 7cc7a124b..35a6dcc4f 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -37,8 +37,10 @@ WindowNewMap::WindowNewMap(QWidget *parent) : show(); //setup initial parameters - mapGenOptions.setWidth(ui->widthTxt->text().toInt()); - mapGenOptions.setHeight(ui->heightTxt->text().toInt()); + int width = ui->widthTxt->text().toInt(); + int height = ui->heightTxt->text().toInt(); + mapGenOptions.setWidth(width ? width : 1); + mapGenOptions.setHeight(height ? height : 1); bool twoLevel = ui->twoLevelCheck->isChecked(); mapGenOptions.setHasTwoLevels(twoLevel); updateTemplateList(); diff --git a/scripting/lua/LuaScriptingContext.cpp b/scripting/lua/LuaScriptingContext.cpp index 8fe13d813..1ecb831ea 100644 --- a/scripting/lua/LuaScriptingContext.cpp +++ b/scripting/lua/LuaScriptingContext.cpp @@ -511,14 +511,14 @@ int LuaContext::loadModule() registar->pushMetatable(L); } - else if(scope == "core") + else if(scope == CModHandler::scopeBuiltin()) { // boost::algorithm::replace_all(modulePath, boost::is_any_of("\\/ "), ""); boost::algorithm::replace_all(modulePath, ".", "/"); - auto loader = CResourceHandler::get("core"); + auto loader = CResourceHandler::get(CModHandler::scopeBuiltin()); modulePath = "scripts/lib/" + modulePath; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 473ad5ae6..40e684973 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -6642,6 +6642,8 @@ void CGameHandler::runBattle() } } } + // 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? diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index bd9e46125..172a8c681 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -1013,6 +1013,10 @@ static void handleCommandOptions(int argc, char * argv[], boost::program_options std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; } } + +#ifdef SINGLE_PROCESS_APP + options.emplace("run-by-client", po::variable_value{true, true}); +#endif po::notify(options); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index bc80920a3..889fccab9 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -89,7 +89,17 @@ bool SaveGame::applyGh(CGameHandler * gh) bool EndTurn::applyGh(CGameHandler * gh) { - PlayerColor player = GS(gh)->currentPlayer; + PlayerColor currentPlayer = GS(gh)->currentPlayer; + if(player != currentPlayer) + { + if(gh->getPlayerStatus(player) == EPlayerStatus::INGAME) + throwAndComplain(gh, "Player attempted to end turn for another player!"); + + logGlobal->debug("Player attempted to end turn after game over. Ignoring this request."); + + return true; + } + throwOnWrongPlayer(gh, player); if(gh->queries.topQuery(player)) throwAndComplain(gh, "Cannot end turn before resolving queries!"); diff --git a/test/scripting/ScriptFixture.cpp b/test/scripting/ScriptFixture.cpp index ae6286311..7eddf455b 100644 --- a/test/scripting/ScriptFixture.cpp +++ b/test/scripting/ScriptFixture.cpp @@ -29,7 +29,7 @@ void ScriptFixture::loadScriptFromFile(const std::string & path) void ScriptFixture::loadScript(const JsonNode & scriptConfig) { - subject = VLC->scriptHandler->loadFromJson(&loggerMock, "core", scriptConfig, "test"); + subject = VLC->scriptHandler->loadFromJson(&loggerMock, CModHandler::scopeBuiltin(), scriptConfig, "test"); GTEST_ASSERT_NE(subject, nullptr); diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index 3eada404c..31d47e121 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -79,7 +79,7 @@ TEST_P(TimedApplyTest, ChangesBonuses) options["cumulative"].Bool() = cumulative; options["bonus"]["test1"] = testBonus1.toJsonNode(); options["bonus"]["test2"] = testBonus2.toJsonNode(); - options.setMeta("core"); + options.setMeta(CModHandler::scopeBuiltin()); setupEffect(options); const uint32_t unitId = 42; diff --git a/vcmibuilder b/vcmibuilder index 919878814..08b78ab31 100755 --- a/vcmibuilder +++ b/vcmibuilder @@ -168,13 +168,19 @@ mkdir -p "$temp_dir" if [[ -n "$gog_file" ]] then - data_dir="$temp_dir"/app + data_dir="$temp_dir"/gog mkdir -p "$data_dir" # innoextract always reports error (iconv 84 error). Just test file for presence test -f "$gog_file" || fail "Error: gog.com executable was not found!" gog_file="$(cd "$(dirname "$gog_file")"; pwd)/$(basename "$gog_file")" cd "$data_dir" && innoextract "$gog_file" + + # some versions of gog.com installer (or innoextract tool?) place game files inside /app directory + if [[ -d "$data_dir"/app ]] + then + data_dir="$data_dir"/app + fi fi if [[ -n "$cd1_dir" ]] @@ -248,10 +254,10 @@ fi if [[ -n "$validate" ]] then - test -f "$dest_dir"/Data/H3bitmap.lod || fail "Error: Heroes 3 data files are missing!" - test -f "$dest_dir"/Data/H3sprite.lod || fail "Error: Heroes 3 data files are missing!" - test -f "$dest_dir"/Data/VIDEO.VID || fail "Error: Heroes 3 data files (CD2) are missing!" - test -d "$dest_dir"/Mods/vcmi/Data || fail "Error: VCMI data files are missing!" + test -f "$dest_dir"/[Dd][Aa][Tt][Aa]/H3bitmap.lod || fail "Error: Heroes 3 data files are missing!" + test -f "$dest_dir"/[Dd][Aa][Tt][Aa]/H3sprite.lod || fail "Error: Heroes 3 data files are missing!" + test -f "$dest_dir"/[Dd][Aa][Tt][Aa]/VIDEO.VID || fail "Error: Heroes 3 data files (CD2) are missing!" + #test -d "$dest_dir"/Mods/vcmi/Data || fail "Error: VCMI data files are missing!" fi rm -rf "$temp_dir"