diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ea3d74630..be65394fc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,6 +15,7 @@ Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,7 +25,7 @@ Steps to reproduce the behavior: A clear and concise description of what you expected to happen. **Actual behavior** -A clear description what is currently happening +A clear description what is currently happening **Did it work earlier?** If this something which worked well some time ago, please let us know about version where it works or at date when it worked. @@ -33,8 +34,9 @@ If this something which worked well some time ago, please let us know about vers If applicable, add screenshots to help explain your problem. **Version** - - OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS] - - Version: [VCMI version] + +- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS] +- Version: [VCMI version] **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 4507ca072..b4beca40b 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -402,3 +402,9 @@ jobs: run: | sudo apt install python3-jstyleson python3 CI/validate_json.py + + - name: Validate Markdown + uses: DavidAnson/markdownlint-cli2-action@v18 + with: + config: 'CI/example.markdownlint-cli2.jsonc' + globs: '**/*.md' diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 655d07a82..d591b8d7d 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); cast.castEval(state->getServerCallback(), ps.dest); - auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; }); + auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); }); auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool { @@ -731,7 +731,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state); } - for(const auto & unit : allUnits) { if(!unit->isValidTarget(true)) @@ -771,11 +770,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); #if BATTLE_TRACE_LEVEL >= 1 - logAi->trace( - "Spell affects %s (%d), dps: %2f", - unit->creatureId().toCreature()->getNameSingularTranslated(), - unit->getCount(), - dpsReduce); + // Ensure ps.dest is not empty before accessing the first element + if (!ps.dest.empty()) + { + logAi->trace( + "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", + ps.spell->getNameTranslated(), + ps.dest.at(0).hexValue.hex, // Safe to access .at(0) now + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce, + oldHealth, + newHealth); + } + else + { + // Handle the case where ps.dest is empty + logAi->trace( + "Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", + ps.spell->getNameTranslated(), + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce, + oldHealth, + newHealth); + } #endif } } diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 0cffe63f5..07576eb1f 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -906,7 +906,7 @@ std::vector BattleExchangeEvaluator::getOneTurnReachableUn { std::vector result; - for(int i = 0; i < turnOrder.size(); i++) + for(int i = 0; i < turnOrder.size(); i++, turn++) { auto & turnQueue = turnOrder[i]; HypotheticBattle turnBattle(env.get(), cb); diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index d0d2d998a..ac23c2149 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -34,11 +34,6 @@ namespace NKAI { -// our to enemy strength ratio constants -const float SAFE_ATTACK_CONSTANT = 1.1f; -const float RETREAT_THRESHOLD = 0.3f; -const double RETREAT_ABSOLUTE_THRESHOLD = 10000.; - //one thread may be turn of AI and another will be handling a side effect for AI2 thread_local CCallback * cb = nullptr; thread_local AIGateway * ai = nullptr; @@ -286,6 +281,9 @@ void AIGateway::tileRevealed(const std::unordered_set & pos) for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile)) addVisitableObj(obj); } + + if (nullkiller->settings->isUpdateHitmapOnTileReveal()) + nullkiller->dangerHitMap->reset(); } void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) @@ -553,7 +551,7 @@ std::optional AIGateway::makeSurrenderRetreatDecision(const Battle 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() && ourStrength < RETREAT_ABSOLUTE_THRESHOLD && fightRatio < RETREAT_THRESHOLD && battleState.canFlee) + if(cb->getTownsInfo().size() && ourStrength < nullkiller->settings->getRetreatThresholdAbsolute() && fightRatio < nullkiller->settings->getRetreatThresholdRelative() && battleState.canFlee) { return BattleAction::makeRetreat(battleState.ourSide); } @@ -670,7 +668,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector (1 / SAFE_ATTACK_CONSTANT); + bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1; answer = !dangerUnknown && !dangerTooHigh; } diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index a8c325f98..4ee0e960e 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -146,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const return h == rhs.get(true); } -bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength) +bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength, float safeAttackRatio) { - const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength(); + const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength(); if(dangerStrength) { - return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength; + return heroStrength > dangerStrength * safeAttackRatio; } return true; //there's no danger } -bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength) +bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio) { - return isSafeToVisit(h, h, dangerStrength); + return isSafeToVisit(h, h, dangerStrength, safeAttackRatio); } bool isObjectRemovable(const CGObjectInstance * obj) diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index 728130ff6..4275bea9f 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -61,11 +61,6 @@ const int GOLD_MINE_PRODUCTION = 1000; const int WOOD_ORE_MINE_PRODUCTION = 2; const int RESOURCE_MINE_PRODUCTION = 1; const int ACTUAL_RESOURCE_COUNT = 7; -const int ALLOWED_ROAMING_HEROES = 8; - -//implementation-dependent -extern const float SAFE_ATTACK_CONSTANT; -extern const int GOLD_RESERVE; extern thread_local CCallback * cb; @@ -213,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos); bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj); bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property! -bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength); -bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength); +bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio); +bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio); bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2); bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2); diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 120252f7d..b366fa176 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -13,6 +13,7 @@ #include "../Engine/Nullkiller.h" #include "../../../CCallback.h" #include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/IGameSettings.h" #include "../../../lib/GameConstants.h" namespace NKAI @@ -152,16 +153,6 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, uint64_t armyValue = 0; TemporaryArmy newArmyInstance; - auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE)); - - for(auto bonus : *bonusModifiers) - { - // army bonuses will change and object bonuses are temporary - if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE) - { - newArmyInstance.addNewBonus(std::make_shared(*bonus)); - } - } while(allowedFactions.size() < alignmentMap.size()) { @@ -197,16 +188,18 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, auto morale = slot.second->moraleVal(); auto multiplier = 1.0f; - const float BadMoraleChance = 0.083f; - const float HighMoraleChance = 0.04f; + const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); + const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); - if(morale < 0) + if(morale < 0 && !badMoraleDice.empty()) { - multiplier += morale * BadMoraleChance; + size_t diceIndex = std::min(badMoraleDice.size(), -morale) - 1; + multiplier -= 1.0 / badMoraleDice.at(diceIndex); } - else if(morale > 0) + else if(morale > 0 && !highMoraleDice.empty()) { - multiplier += morale * HighMoraleChance; + size_t diceIndex = std::min(highMoraleDice.size(), morale) - 1; + multiplier += 1.0 / highMoraleDice.at(diceIndex); } newValue += multiplier * slot.second->getPower(); diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 55a1ebeaf..01dfa0a82 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) for(int upgradeIndex : {1, 0}) { BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex)); - if(!vstd::contains(buildings, building)) continue; // no such building in town @@ -73,11 +72,18 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) { - otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE}); otherBuildings.push_back({BuildingID::HORDE_1}); otherBuildings.push_back({BuildingID::HORDE_2}); } + otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE }); + otherBuildings.push_back({ BuildingID::RESOURCE_SILO }); + otherBuildings.push_back({ BuildingID::SPECIAL_1 }); + otherBuildings.push_back({ BuildingID::SPECIAL_2 }); + otherBuildings.push_back({ BuildingID::SPECIAL_3 }); + otherBuildings.push_back({ BuildingID::SPECIAL_4 }); + otherBuildings.push_back({ BuildingID::MARKETPLACE }); + for(auto & buildingSet : otherBuildings) { for(auto & buildingID : buildingSet) @@ -141,6 +147,8 @@ void BuildAnalyzer::update() auto towns = ai->cb->getTownsInfo(); + float economyDevelopmentCost = 0; + for(const CGTownInstance* town : towns) { logAi->trace("Checking town %s", town->getNameTranslated()); @@ -153,6 +161,11 @@ void BuildAnalyzer::update() requiredResources += developmentInfo.requiredResources; totalDevelopmentCost += developmentInfo.townDevelopmentCost; + for(auto building : developmentInfo.toBuild) + { + if (building.dailyIncome[EGameResID::GOLD] > 0) + economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD]; + } armyCost += developmentInfo.armyCost; for(auto bi : developmentInfo.toBuild) @@ -171,15 +184,7 @@ void BuildAnalyzer::update() updateDailyIncome(); - if(ai->cb->getDate(Date::DAY) == 1) - { - goldPressure = 1; - } - else - { - goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f - + (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); - } + goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); logAi->trace("Gold pressure: %f", goldPressure); } @@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( logAi->trace("checking %s", info.name); logAi->trace("buildInfo %s", info.toString()); + int highestFort = 0; + for (auto twn : ai->cb->getTownsInfo()) + { + highestFort = std::max(highestFort, (int)twn->fortLevel()); + } + if(!town->hasBuilt(building)) { auto canBuild = ai->cb->canBuildStructure(town, building); @@ -281,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( prerequisite.baseCreatureID = info.baseCreatureID; prerequisite.prerequisitesCount++; prerequisite.armyCost = info.armyCost; - prerequisite.dailyIncome = info.dailyIncome; + bool haveSameOrBetterFort = false; + if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT) + haveSameOrBetterFort = true; + if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL) + haveSameOrBetterFort = true; + if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE) + haveSameOrBetterFort = true; + if(!haveSameOrBetterFort) + prerequisite.dailyIncome = info.dailyIncome; return prerequisite; } diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index cd9c4806d..5a8d0a24f 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap() heroes[hero->tempOwner][hero] = HeroRole::MAIN; } - if(obj->ID == Obj::TOWN) { auto town = dynamic_cast(obj); @@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap() newThreat.hero = path.targetHero; newThreat.turn = path.turn(); + newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0); newThreat.danger = path.getHeroStrength(); if(newThreat.value() > node.maximumDanger.value()) @@ -316,8 +316,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & const auto& info = getTileThreat(tile); - return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger)) - || (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger)); + return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger, ai->settings->getSafeAttackRatio())) + || (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger, ai->settings->getSafeAttackRatio())); } const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const @@ -348,6 +348,7 @@ std::set DangerHitMapAnalyzer::getOneTurnAccessibleObj void DangerHitMapAnalyzer::reset() { hitMapUpToDate = false; + tileOwnersUpToDate = false; } } diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index fc2890846..2bd39a2d8 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -22,6 +22,7 @@ struct HitMapInfo uint64_t danger; uint8_t turn; + float threat; HeroPtr hero; HitMapInfo() @@ -33,6 +34,7 @@ struct HitMapInfo { danger = 0; turn = 255; + threat = 0; hero = HeroPtr(); } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 1f4b97c0c..be3526fda 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const { - return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f; + return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE); } void HeroManager::update() @@ -108,7 +108,7 @@ void HeroManager::update() for(auto & hero : myHeroes) { scores[hero] = evaluateFightingStrength(hero); - knownFightingStrength[hero->id] = hero->getFightingStrength(); + knownFightingStrength[hero->id] = hero->getHeroStrength(); } auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool @@ -147,7 +147,10 @@ void HeroManager::update() HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const { - return heroRoles.at(hero); + if (heroRoles.find(hero) != heroRoles.end()) + return heroRoles.at(hero); + else + return HeroRole::SCOUT; } const std::map & HeroManager::getHeroRoles() const @@ -188,13 +191,11 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const return evaluateFightingStrength(hero); } -bool HeroManager::heroCapReached() const +bool HeroManager::heroCapReached(bool includeGarrisoned) const { - const bool includeGarnisoned = true; - int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); + int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned); - return heroCount >= ALLOWED_ROAMING_HEROES - || heroCount >= ai->settings->getMaxRoamingHeroes() + return heroCount >= ai->settings->getMaxRoamingHeroes() || heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) || heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP); } @@ -204,7 +205,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const auto cached = knownFightingStrength.find(hero->id); //FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?) - return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength(); + return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength(); } float HeroManager::getMagicStrength(const CGHeroInstance * hero) const @@ -281,7 +282,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const return nullptr; } -const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const +const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const { const CGHeroInstance * weakestHero = nullptr; auto myHeroes = ai->cb->getHeroesInfo(); @@ -292,12 +293,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co || existingHero->getArmyStrength() >armyLimit || getHeroRole(existingHero) == HeroRole::MAIN || existingHero->movementPointsRemaining() + || (townToSpare != nullptr && existingHero->visitedTown == townToSpare) || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) { continue; } - if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) + if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength()) { weakestHero = existingHero; } diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 675357626..383f3c13a 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -56,9 +56,9 @@ public: float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const; float evaluateHero(const CGHeroInstance * hero) const; bool canRecruitHero(const CGTownInstance * t = nullptr) const; - bool heroCapReached() const; + bool heroCapReached(bool includeGarrisoned = true) const; const CGHeroInstance * findHeroWithGrail() const; - const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const; + const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const; float getMagicStrength(const CGHeroInstance * hero) const; float getFightingStrengthCached(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 5c628f288..5b9e0a12b 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -97,9 +97,10 @@ std::optional ObjectClusterizer::getBlocker(const AIPa { auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord); - blockers = ai->cb->getVisitableObjs(node.coord); + if (ai->cb->isVisible(node.coord)) + blockers = ai->cb->getVisitableObjs(node.coord); - if(guardPos.valid()) + if(guardPos.valid() && ai->cb->isVisible(guardPos)) { auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord)); @@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject( heroesProcessed.insert(path.targetHero); - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER); - if(priority < MIN_PRIORITY) + if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) + continue; + else if (priority <= 0) continue; ClusterMap::accessor cluster; @@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject( heroesProcessed.insert(path.targetHero); - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER); - if(priority < MIN_PRIORITY) + if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) + continue; + else if (priority <= 0) continue; bool interestingObject = path.turn() <= 2 || priority > 0.5f; diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index 2cdc2ead3..50f800913 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -49,26 +49,49 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo(); auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh(); + ai->dangerHitMap->updateHitMap(); + for(auto & developmentInfo : developmentInfos) { - for(auto & buildingInfo : developmentInfo.toBuild) + bool emergencyDefense = false; + uint8_t closestThreat = std::numeric_limits::max(); + for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town)) { - if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) + closestThreat = std::min(closestThreat, threat.turn); + } + for (auto& buildingInfo : developmentInfo.toBuild) + { + if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes) { - if(buildingInfo.notEnoughRes) + if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE) { - if(ai->getLockedResources().canAfford(buildingInfo.buildCost)) - continue; - - Composition composition; - - composition.addNext(BuildThis(buildingInfo, developmentInfo)); - composition.addNext(SaveResources(buildingInfo.buildCost)); - - tasks.push_back(sptr(composition)); - } - else tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); + emergencyDefense = true; + } + } + } + if (!emergencyDefense) + { + for (auto& buildingInfo : developmentInfo.toBuild) + { + if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) + { + if (buildingInfo.notEnoughRes) + { + if (ai->getLockedResources().canAfford(buildingInfo.buildCost)) + continue; + + Composition composition; + + composition.addNext(BuildThis(buildingInfo, developmentInfo)); + composition.addNext(SaveResources(buildingInfo.buildCost)); + tasks.push_back(sptr(composition)); + } + else + { + tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); + } + } } } } diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index d53adc023..738196e2d 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const { Goals::TGoalVec tasks; - if(ai->cb->getDate(Date::DAY) == 1) - return tasks; - auto heroes = cb->getHeroesInfo(); if(heroes.empty()) @@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const return tasks; } + ai->dangerHitMap->updateHitMap(); + for(auto town : cb->getTownsInfo()) { + uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn; + + if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN) + { + return tasks; + } + auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet( town, ai->getFreeResources()); for(const CGHeroInstance * targetHero : heroes) { - if(ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL)) - { - continue; - } - if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN) { auto reinforcement = ai->armyManager->howManyReinforcementsCanGet( diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index 576dedfeb..38e71b675 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals( logAi->trace("Path found %s", path.toString()); #endif - if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength()); -#endif - continue; - } - if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit)) { #if NKAI_TRACE_LEVEL >= 2 @@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals( auto hero = path.targetHero; auto danger = path.getTotalDanger(); + if (hero->getOwner() != nullkiller->playerID) + continue; + if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && (path.getTotalDanger() == 0 || path.turn() > 0) && path.exchangeCount > 1) @@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals( continue; } - auto isSafe = isSafeToVisit(hero, path.heroArmy, danger); + auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio()); #if NKAI_TRACE_LEVEL >= 2 logAi->trace( diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index e23efcb1c..86bd7e77c 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const for(auto town : ai->cb->getTownsInfo()) { evaluateDefence(tasks, town, ai); + //Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice + if (!tasks.empty()) + break; } return tasks; @@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - return true; + return false; } else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN) { @@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa { tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); - return true; + return false; } } } @@ -158,11 +161,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there - if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai)) + if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai)) { return; } - if(!threatNode.fastestDanger.hero) { logAi->trace("No threat found for town %s", town->getNameTranslated()); @@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } + if (!path.targetHero->canBeMergedWith(*town)) + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Can't merge armies of hero %s and town %s", + path.targetHero->getObjectName(), + town->getObjectName()); +#endif + continue; + } + if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1) { #if NKAI_TRACE_LEVEL >= 1 @@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta // dismiss creatures we are not able to pick to be able to hide in garrison if(town->garrisonHero || town->getUpperArmy()->stacksCount() == 0 + || path.targetHero->canBeMergedWith(*town) || (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL)) { tasks.push_back( @@ -292,7 +305,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } - if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger)) + if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * ai->settings->getSafeAttackRatio() >= threat.danger)) { if(ai->arePathHeroesLocked(path)) { @@ -343,23 +356,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta } else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) { - if(town->garrisonHero) + if(town->garrisonHero && town->garrisonHero != path.targetHero) { - if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT - && town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20) - { - if(path.turn() == 0) - sequence.push_back(sptr(DismissHero(town->visitingHero.get()))); - } - else - { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", - path.targetHero->getObjectName(), - town->getObjectName()); + logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", + path.targetHero->getObjectName(), + town->getObjectName()); #endif - continue; - } + continue; } else if(path.turn() == 0) { @@ -405,6 +409,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const { + if (threat.turn > 0 || town->garrisonHero || town->visitingHero) + return; + if(town->hasBuilt(BuildingID::TAVERN) && ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) { @@ -451,7 +458,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM } else if(ai->heroManager->heroCapReached()) { - heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength()); + heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town); if(!heroToDismiss) continue; diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp index 4a210d2c0..b9c6e568d 100644 --- a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp @@ -33,48 +33,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const { Goals::TGoalVec tasks; - for(auto obj : ai->memory->visitableObjs) + for (auto obj : ai->memory->visitableObjs) { - if(!vstd::contains(ai->memory->alreadyVisited, obj)) + switch (obj->ID.num) { - switch(obj->ID.num) - { case Obj::REDWOOD_OBSERVATORY: case Obj::PILLAR_OF_FIRE: - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); + { + auto rObj = dynamic_cast(obj); + if (!rObj->wasScouted(ai->playerID)) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); break; + } case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_TWO_WAY: case Obj::SUBTERRANEAN_GATE: case Obj::WHIRLPOOL: - auto tObj = dynamic_cast(obj); - if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability) - { - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); - } - break; - } - } - else - { - switch(obj->ID.num) { - case Obj::MONOLITH_TWO_WAY: - case Obj::SUBTERRANEAN_GATE: - case Obj::WHIRLPOOL: - auto tObj = dynamic_cast(obj); - if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability) - break; - for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits) + auto tObj = dynamic_cast(obj); + for (auto exit : cb->getTeleportChannelExits(tObj->channel)) { - if(!cb->getObj(exit)) - { - // Always attempt to visit two-way teleports if one of channel exits is not visible - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); - break; + if (exit != tObj->id) + { + if (!cb->isVisible(cb->getObjInstance(exit))) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); } } - break; } } } diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index b9a675837..b6b0811cb 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif + if (path.targetHero->getOwner() != ai->playerID) + continue; + if(path.containsHero(hero)) { #if NKAI_TRACE_LEVEL >= 2 @@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con continue; } - if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); -#endif - continue; - } - if(ai->arePathHeroesLocked(path)) { #if NKAI_TRACE_LEVEL >= 2 @@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con } auto danger = path.getTotalDanger(); - auto isSafe = isSafeToVisit(hero, path.heroArmy, danger); + auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, ai->settings->getSafeAttackRatio()); #if NKAI_TRACE_LEVEL >= 2 logAi->trace( @@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT continue; } - auto heroRole = ai->heroManager->getHeroRole(path.targetHero); - - if(heroRole == HeroRole::SCOUT - && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); -#endif - continue; - } - auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); if(!upgrader->garrisonHero @@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength(); - armyToGetOrBuy.addArmyToBuy( - ai->armyManager->toSlotInfo( - ai->armyManager->getArmyAvailableToBuy( - path.heroArmy, - upgrader, - ai->getFreeResources(), - path.turn()))); - upgrade.upgradeValue += armyToGetOrBuy.upgradeValue; upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); @@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT { for(auto hero : cb->getAvailableHeroes(upgrader)) { - auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader) - + ai->armyManager->howManyReinforcementsCanGet(hero, upgrader); + auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader); if(scoutReinforcement >= armyToGetOrBuy.upgradeValue && ai->getFreeGold() >20000 @@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT auto danger = path.getTotalDanger(); - auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger); + auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger, ai->settings->getSafeAttackRatio()); #if NKAI_TRACE_LEVEL >= 2 logAi->trace( diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index f086b62cd..16c19ed62 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const auto ourHeroes = ai->heroManager->getHeroRoles(); auto minScoreToHireMain = std::numeric_limits::max(); + int currentArmyValue = 0; for(auto hero : ourHeroes) { + currentArmyValue += hero.first->getArmyCost(); if(hero.second != HeroRole::MAIN) continue; @@ -45,51 +47,88 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const minScoreToHireMain = newScore; } } + // If we don't have any heros we might want to lower our expectations. + if (ourHeroes.empty()) + minScoreToHireMain = 0; + const CGHeroInstance* bestHeroToHire = nullptr; + const CGTownInstance* bestTownToHireFrom = nullptr; + float bestScore = 0; + bool haveCapitol = false; + + ai->dangerHitMap->updateHitMap(); + int treasureSourcesCount = 0; + for(auto town : towns) { + uint8_t closestThreat = UINT8_MAX; + for (auto threat : ai->dangerHitMap->getTownThreats(town)) + { + closestThreat = std::min(closestThreat, threat.turn); + } + //Don't hire a hero where there already is one present + if (town->visitingHero && town->garrisonHero) + continue; + float visitability = 0; + for (auto checkHero : ourHeroes) + { + if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town) + visitability++; + } if(ai->heroManager->canRecruitHero(town)) { auto availableHeroes = ai->cb->getAvailableHeroes(town); - - for(auto hero : availableHeroes) + + for (auto obj : ai->objectClusterizer->getNearbyObjects()) { - auto score = ai->heroManager->evaluateHero(hero); - - if(score > minScoreToHireMain) - { - tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200))); - break; - } - } - - int treasureSourcesCount = 0; - - for(auto obj : ai->objectClusterizer->getNearbyObjects()) - { - if((obj->ID == Obj::RESOURCE) + if ((obj->ID == Obj::RESOURCE) || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE || isWeeklyRevisitable(ai, obj) - || obj->ID ==Obj::ARTIFACT) + || obj->ID == Obj::ARTIFACT) { auto tile = obj->visitablePos(); auto closestTown = ai->dangerHitMap->getClosestTown(tile); - if(town == closestTown) + if (town == closestTown) treasureSourcesCount++; } } - if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000)) - continue; - - if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1 - || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh())) + for(auto hero : availableHeroes) { - tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3))); + auto score = ai->heroManager->evaluateHero(hero); + if(score > minScoreToHireMain) + { + score *= score / minScoreToHireMain; + } + score *= (hero->getArmyCost() + currentArmyValue); + if (hero->getFactionID() == town->getFactionID()) + score *= 1.5; + if (vstd::isAlmostZero(visitability)) + score *= 30 * town->getTownLevel(); + else + score *= town->getTownLevel() / visitability; + if (score > bestScore) + { + bestScore = score; + bestHeroToHire = hero; + bestTownToHireFrom = town; + } } } + if (town->hasCapitol()) + haveCapitol = true; + } + if (bestHeroToHire && bestTownToHireFrom) + { + if (ai->cb->getHeroesInfo().size() == 0 + || treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5 + || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol) + || (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh())) + { + tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1)))); + } } return tasks; diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp index 595830a66..1b2e0a04b 100644 --- a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp @@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const for(auto town : towns) { - if(!town->hasBuilt(BuildingID::MAGES_GUILD_1)) - continue; - ai->pathfinder->calculatePathInfo(paths, town->visitablePos()); for(auto & path : paths) @@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const if(town->visitingHero && town->visitingHero.get() != path.targetHero) continue; - if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit()) - continue; - - if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1) + if(!path.getFirstBlockedAction() && path.exchangeCount <= 1) { - if(path.targetHero->mana == path.targetHero->manaLimit()) - continue; - Composition stayAtTown; stayAtTown.addNextSequence({ diff --git a/AI/Nullkiller/Engine/FuzzyEngines.cpp b/AI/Nullkiller/Engine/FuzzyEngines.cpp index 7659f1352..a05119d90 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.cpp +++ b/AI/Nullkiller/Engine/FuzzyEngines.cpp @@ -17,8 +17,7 @@ namespace NKAI { -#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter -#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us +constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter engineBase::engineBase() { diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index 3907d5df3..a5fcd6a1a 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit { objectDanger += evaluateDanger(hero->visitedTown.get()); } + objectDanger *= ai->heroManager->getFightingStrengthCached(hero); + } + if (objWithID(dangerousObject)) + { + auto town = dynamic_cast(dangerousObject); + auto hero = town->garrisonHero; + + if (hero) + objectDanger *= ai->heroManager->getFightingStrengthCached(hero); } if(objectDanger) @@ -117,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) { auto fortLevel = town->fortLevel(); - if(fortLevel == CGTownInstance::EFortLevel::CASTLE) - danger += 10000; + if (fortLevel == CGTownInstance::EFortLevel::CASTLE) + danger = std::max(danger * 2, danger + 10000); else if(fortLevel == CGTownInstance::EFortLevel::CITADEL) - danger += 4000; + danger = std::max(ui64(danger * 1.4), danger + 4000); } return danger; diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index fa15363f6..c21f1c57d 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -34,13 +34,12 @@ using namespace Goals; std::unique_ptr Nullkiller::baseGraph; Nullkiller::Nullkiller() - :activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true) + : activeHero(nullptr) + , scanDepth(ScanDepth::MAIN_FULL) + , useHeroChain(true) + , memory(std::make_unique()) { - memory = std::make_unique(); - settings = std::make_unique(); - useObjectGraph = settings->isObjectGraphAllowed(); - openMap = settings->isOpenMap() || useObjectGraph; } bool canUseOpenMap(std::shared_ptr cb, PlayerColor playerID) @@ -62,17 +61,23 @@ bool canUseOpenMap(std::shared_ptr cb, PlayerColor playerID) return false; } - return cb->getStartInfo()->difficulty >= 3; + return true; } void Nullkiller::init(std::shared_ptr cb, AIGateway * gateway) { this->cb = cb; this->gateway = gateway; - - playerID = gateway->playerID; + this->playerID = gateway->playerID; - if(openMap && !canUseOpenMap(cb, playerID)) + settings = std::make_unique(cb->getStartInfo()->difficulty); + + if(canUseOpenMap(cb, playerID)) + { + useObjectGraph = settings->isObjectGraphAllowed(); + openMap = settings->isOpenMap() || useObjectGraph; + } + else { useObjectGraph = false; openMap = false; @@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task) { TGoalVec blockers; + if (task->asTask()->priority <= 0) + return; + for(auto & item : tasks) { for(auto objid : item.affectedObjects) { - if(task == item.task || task->asTask()->isObjectAffected(objid)) + if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero())) { if(item.task->asTask()->priority >= task->asTask()->priority) return; @@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const return taskptr(*bestTask); } -Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const +Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const { TaskPlan taskPlan; - tbb::parallel_for(tbb::blocked_range(0, tasks.size()), [this, &tasks](const tbb::blocked_range & r) + tbb::parallel_for(tbb::blocked_range(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range & r) { auto evaluator = this->priorityEvaluators->acquire(); for(size_t i = r.begin(); i != r.end(); i++) { auto task = tasks[i]; - - if(task->asTask()->priority <= 0) - task->asTask()->priority = evaluator->evaluate(task); + if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS) + task->asTask()->priority = evaluator->evaluate(task, priorityTier); } }); @@ -326,7 +333,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const if(lockReason != HeroLockedReason::NOT_LOCKED) { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString()); + logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason, path.toString()); #endif return true; } @@ -347,12 +354,24 @@ void Nullkiller::makeTurn() boost::lock_guard sharedStorageLock(AISharedStorage::locker); const int MAX_DEPTH = 10; - const float FAST_TASK_MINIMAL_PRIORITY = 0.7f; resetAiState(); Goals::TGoalVec bestTasks; +#if NKAI_TRACE_LEVEL >= 1 + float totalHeroStrength = 0; + int totalTownLevel = 0; + for (auto heroInfo : cb->getHeroesInfo()) + { + totalHeroStrength += heroInfo->getTotalStrength(); + } + for (auto townInfo : cb->getTownsInfo()) + { + totalTownLevel += townInfo->getTownLevel(); + } + logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); +#endif for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) { auto start = std::chrono::high_resolution_clock::now(); @@ -360,17 +379,21 @@ void Nullkiller::makeTurn() Goals::TTask bestTask = taskptr(Goals::Invalid()); - for(;i <= settings->getMaxPass(); i++) + while(true) { bestTasks.clear(); + decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(BuyArmyBehavior()), 1); decompose(bestTasks, sptr(BuildingBehavior()), 1); bestTask = choseBestTask(bestTasks); - if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY) + if(bestTask->priority > 0) { +#if NKAI_TRACE_LEVEL >= 1 + logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority); +#endif if(!executeTask(bestTask)) return; @@ -382,7 +405,6 @@ void Nullkiller::makeTurn() } } - decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1); decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH); @@ -392,12 +414,24 @@ void Nullkiller::makeTurn() if(!isOpenMap()) decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); - if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty()) + TTaskVec selectedTasks; +#if NKAI_TRACE_LEVEL >= 1 + int prioOfTask = 0; +#endif + for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio) { - decompose(bestTasks, sptr(StartupBehavior()), 1); +#if NKAI_TRACE_LEVEL >= 1 + prioOfTask = prio; +#endif + selectedTasks = buildPlan(bestTasks, prio); + if (!selectedTasks.empty() || settings->isUseFuzzy()) + break; } - auto selectedTasks = buildPlan(bestTasks); + std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b) + { + return a->priority > b->priority; + }); logAi->debug("Decision madel in %ld", timeElapsed(start)); @@ -438,7 +472,7 @@ void Nullkiller::makeTurn() bestTask->priority); } - if(bestTask->priority < MIN_PRIORITY) + if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0)) { auto heroes = cb->getHeroesInfo(); auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool @@ -463,7 +497,9 @@ void Nullkiller::makeTurn() continue; } - +#if NKAI_TRACE_LEVEL >= 1 + logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority); +#endif if(!executeTask(bestTask)) { if(hasAnySuccess) @@ -471,13 +507,27 @@ void Nullkiller::makeTurn() else return; } - hasAnySuccess = true; } + hasAnySuccess |= handleTrading(); + if(!hasAnySuccess) { logAi->trace("Nothing was done this turn. Ending turn."); +#if NKAI_TRACE_LEVEL >= 1 + totalHeroStrength = 0; + totalTownLevel = 0; + for (auto heroInfo : cb->getHeroesInfo()) + { + totalHeroStrength += heroInfo->getTotalStrength(); + } + for (auto townInfo : cb->getTownsInfo()) + { + totalTownLevel += townInfo->getTownLevel(); + } + logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); +#endif return; } @@ -554,4 +604,102 @@ void Nullkiller::lockResources(const TResources & res) lockedResources += res; } +bool Nullkiller::handleTrading() +{ + bool haveTraded = false; + bool shouldTryToTrade = true; + int marketId = -1; + for (auto town : cb->getTownsInfo()) + { + if (town->hasBuiltSomeTradeBuilding()) + { + marketId = town->id; + } + } + if (marketId == -1) + return false; + if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false)) + { + if (const auto* m = dynamic_cast(obj)) + { + while (shouldTryToTrade) + { + shouldTryToTrade = false; + buildAnalyzer->update(); + TResources required = buildAnalyzer->getTotalResourcesRequired(); + TResources income = buildAnalyzer->getDailyIncome(); + TResources available = cb->getResourceAmount(); +#if NKAI_TRACE_LEVEL >= 2 + logAi->debug("Available %s", available.toString()); + logAi->debug("Required %s", required.toString()); +#endif + int mostWanted = -1; + int mostExpendable = -1; + float minRatio = std::numeric_limits::max(); + float maxRatio = std::numeric_limits::min(); + + for (int i = 0; i < required.size(); ++i) + { + if (required[i] <= 0) + continue; + float ratio = static_cast(available[i]) / required[i]; + + if (ratio < minRatio) { + minRatio = ratio; + mostWanted = i; + } + } + + for (int i = 0; i < required.size(); ++i) + { + float ratio = available[i]; + if (required[i] > 0) + ratio = static_cast(available[i]) / required[i]; + else + ratio = available[i]; + + bool okToSell = false; + + if (i == GameResID::GOLD) + { + if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh()) + okToSell = true; + } + else + { + if (required[i] <= 0 && income[i] > 0) + okToSell = true; + } + + if (ratio > maxRatio && okToSell) { + maxRatio = ratio; + mostExpendable = i; + } + } +#if NKAI_TRACE_LEVEL >= 2 + logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted); +#endif + if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1) + return false; + + int toGive; + int toGet; + m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); + //logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName()); + //TODO trade only as much as needed + if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources + { + cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive); +#if NKAI_TRACE_LEVEL >= 1 + logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); +#endif + haveTraded = true; + shouldTryToTrade = true; + } + } + } + } + return haveTraded; +} + } diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index af05e354b..941e71f16 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -120,13 +120,14 @@ public: ScanDepth getScanDepth() const { return scanDepth; } bool isOpenMap() const { return openMap; } bool isObjectGraphAllowed() const { return useObjectGraph; } + bool handleTrading(); private: void resetAiState(); void updateAiState(int pass, bool fast = false); void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const; Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const; - Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const; + Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const; bool executeTask(Goals::TTask task); bool areAffectedObjectsPresent(Goals::TTask task) const; HeroRole getTaskRole(Goals::TTask task) const; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 2290fda00..840f5052f 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -15,6 +15,8 @@ #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/mapping/CMapDefines.h" +#include "../../../lib/RoadHandler.h" #include "../../../lib/CCreatureHandler.h" #include "../../../lib/VCMI_Lib.h" #include "../../../lib/StartInfo.h" @@ -33,11 +35,9 @@ namespace NKAI { -#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter -#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us -const float MIN_CRITICAL_VALUE = 2.0f; +constexpr float MIN_CRITICAL_VALUE = 2.0f; -EvaluationContext::EvaluationContext(const Nullkiller * ai) +EvaluationContext::EvaluationContext(const Nullkiller* ai) : movementCost(0.0), manaCost(0), danger(0), @@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai) heroRole(HeroRole::SCOUT), turn(0), strategicalValue(0), + conquestValue(0), evaluator(ai), enemyHeroDangerRatio(0), - armyGrowth(0) + threat(0), + armyGrowth(0), + armyInvolvement(0), + defenseValue(0), + isDefend(false), + threatTurns(INT_MAX), + involvesSailing(false), + isTradeBuilding(false), + isExchange(false), + isArmyUpgrade(false), + isHero(false), + isEnemy(false), + explorePriority(0) { } @@ -225,7 +238,7 @@ int getDwellingArmyCost(const CGObjectInstance * target) auto creature = creLevel.second.back().toCreature(); auto creaturesAreFree = creature->getLevel() == 1; if(!creaturesAreFree) - cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first; + cost += creature->getFullRecruitCost().marketValue() * creLevel.first; } } @@ -251,6 +264,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art) switch(art->aClass) { + case CArtifact::EartClass::ART_TREASURE: + //FALL_THROUGH case CArtifact::EartClass::ART_MINOR: classValue = 1000; break; @@ -289,6 +304,8 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR4: return getDwellingArmyValue(ai->cb.get(), target, checkGold); + case Obj::SPELL_SCROLL: + //FALL_THROUGH case Obj::ARTIFACT: return evaluateArtifactArmyValue(dynamic_cast(target)->storedArtifact->getType()); case Obj::HERO: @@ -479,7 +496,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const return result; } -uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const +float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const { return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast(hero->mana) / hero->manaLimit())); } @@ -581,6 +598,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons return 0; } +float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const +{ + if (!target) + return 0; + if (target->getOwner() == ai->playerID) + return 0; + switch (target->ID) + { + case Obj::TOWN: + { + if (ai->buildAnalyzer->getDevelopmentInfo().empty()) + return 10.0f; + + auto town = dynamic_cast(target); + + if (town->getOwner() == ai->playerID) + { + auto armyIncome = townArmyGrowth(town); + auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; + + return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f); + } + + auto fortLevel = town->fortLevel(); + auto booster = 1.0f; + + if (town->hasCapitol()) + return booster * 1.5; + + if (fortLevel < CGTownInstance::CITADEL) + return booster * (town->hasFort() ? 1.0 : 0.8); + else + return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2); + } + + case Obj::HERO: + return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES + ? getEnemyHeroStrategicalValue(dynamic_cast(target)) + : 0; + + case Obj::KEYMASTER: + return 0.6f; + + default: + return 0; + } +} + float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const { auto rewardable = dynamic_cast(hut); @@ -705,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army) for(auto stack : army->Slots()) { - value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count; + value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count; } return value; @@ -786,7 +851,9 @@ public: uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai); evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength()); + evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength(); evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero); + evaluationContext.isExchange = true; } }; @@ -804,6 +871,7 @@ public: evaluationContext.armyReward += upgradeValue; evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); + evaluationContext.isArmyUpgrade = true; } }; @@ -818,22 +886,46 @@ public: int tilesDiscovered = task->value; evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered); + for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile)) + { + switch (obj->ID.num) + { + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_TWO_WAY: + case Obj::SUBTERRANEAN_GATE: + evaluationContext.explorePriority = 1; + break; + case Obj::REDWOOD_OBSERVATORY: + case Obj::PILLAR_OF_FIRE: + evaluationContext.explorePriority = 2; + break; + } + } + if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD) + evaluationContext.explorePriority = 1; + if (evaluationContext.explorePriority == 0) + evaluationContext.explorePriority = 3; } }; class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder { public: - void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override { - if(task->goalType != Goals::STAY_AT_TOWN) + if (task->goalType != Goals::STAY_AT_TOWN) return; - Goals::StayAtTown & stayAtTown = dynamic_cast(*task); + Goals::StayAtTown& stayAtTown = dynamic_cast(*task); evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero()); - evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); - evaluationContext.movementCost += stayAtTown.getMovementWasted(); + if (evaluationContext.armyReward == 0) + evaluationContext.isDefend = true; + else + { + evaluationContext.movementCost += stayAtTown.getMovementWasted(); + evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); + } } }; @@ -844,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin if(enemyDanger.danger) { auto dangerRatio = enemyDanger.danger / (double)ourStrength; - auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false); - bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb); - - if(isAI) - { - dangerRatio *= 1.5; // lets make AI bit more afraid of other AI. - } - vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio); + vstd::amax(evaluationContext.threat, enemyDanger.threat); } } @@ -896,6 +981,10 @@ public: else evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); + evaluationContext.defenseValue = town->fortLevel(); + evaluationContext.isDefend = true; + evaluationContext.threatTurns = treat.turn; + vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } @@ -926,6 +1015,8 @@ public: for(auto & node : path.nodes) { vstd::amax(costsPerHero[node.targetHero], node.cost); + if (node.layer == EPathfindingLayer::SAIL) + evaluationContext.involvesSailing = true; } for(auto pair : costsPerHero) @@ -952,10 +1043,18 @@ public: evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole); evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target)); + evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target); + if (target->ID == Obj::HERO) + evaluationContext.isHero = true; + if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES) + evaluationContext.isEnemy = true; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); + evaluationContext.armyInvolvement += army->getArmyCost(); + if(evaluationContext.danger > 0) + evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength(); } - vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); + vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength()); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength()); vstd::amax(evaluationContext.turn, path.turn()); } @@ -996,6 +1095,7 @@ public: evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost; evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost); + evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost; @@ -1021,6 +1121,14 @@ public: Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast(*task); const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero(); + logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size()); + for (auto obj : swapCommand.getAffectedObjects()) + { + logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName()); + } + if (garrisonHero) + logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason())); + if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE) { auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero); @@ -1029,6 +1137,9 @@ public: evaluationContext.movementCost += mpLeft; evaluationContext.movementCostByRole[defenderRole] += mpLeft; evaluationContext.heroRole = defenderRole; + evaluationContext.isDefend = true; + evaluationContext.armyInvolvement = garrisonHero->getArmyStrength(); + logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend); } } }; @@ -1072,8 +1183,14 @@ public: evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; - evaluationContext.goldCost += bi.buildCostWithPrerequisites[EGameResID::GOLD]; + int32_t cost = bi.buildCost[EGameResID::GOLD]; + evaluationContext.goldCost += cost; evaluationContext.closestWayRatio = 1; + evaluationContext.buildingCost += bi.buildCostWithPrerequisites; + if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0) + evaluationContext.isTradeBuilding = true; + + logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue()); if(bi.creatureID != CreatureID::NONE) { @@ -1100,7 +1217,18 @@ public: else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) { evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); + for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo()) + { + evaluationContext.armyInvolvement += hero->getArmyCost(); + } } + int sameTownBonus = 0; + for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo()) + { + if (buildThis.town->getFaction() == town->getFaction()) + sameTownBonus += town->getTownLevel(); + } + evaluationContext.armyReward *= sameTownBonus; if(evaluationContext.goldReward) { @@ -1162,6 +1290,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal for(auto subgoal : parts) { context.goldCost += subgoal->goldCost; + context.buildingCost += subgoal->buildingCost; for(auto builder : evaluationContextBuilders) { @@ -1172,7 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal return context; } -float PriorityEvaluator::evaluate(Goals::TSubgoal task) +float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { auto evaluationContext = buildEvaluationContext(task); @@ -1185,36 +1314,257 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) double result = 0; - try + if (ai->settings->isUseFuzzy()) { - armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); - heroRoleVariable->setValue(evaluationContext.heroRole); - mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); - scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); - goldRewardVariable->setValue(goldRewardPerTurn); - armyRewardVariable->setValue(evaluationContext.armyReward); - armyGrowthVariable->setValue(evaluationContext.armyGrowth); - skillRewardVariable->setValue(evaluationContext.skillReward); - dangerVariable->setValue(evaluationContext.danger); - rewardTypeVariable->setValue(rewardType); - closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio); - strategicalValueVariable->setValue(evaluationContext.strategicalValue); - goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); - goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); - turnVariable->setValue(evaluationContext.turn); - fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); + float fuzzyResult = 0; + try + { + armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); + heroRoleVariable->setValue(evaluationContext.heroRole); + mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); + scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); + goldRewardVariable->setValue(goldRewardPerTurn); + armyRewardVariable->setValue(evaluationContext.armyReward); + armyGrowthVariable->setValue(evaluationContext.armyGrowth); + skillRewardVariable->setValue(evaluationContext.skillReward); + dangerVariable->setValue(evaluationContext.danger); + rewardTypeVariable->setValue(rewardType); + closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio); + strategicalValueVariable->setValue(evaluationContext.strategicalValue); + goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); + goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); + turnVariable->setValue(evaluationContext.turn); + fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); - engine->process(); + engine->process(); - result = value->getValue(); + fuzzyResult = value->getValue(); + } + catch (fl::Exception& fe) + { + logAi->error("evaluate VisitTile: %s", fe.getWhat()); + } + result = fuzzyResult; } - catch(fl::Exception & fe) + else { - logAi->error("evaluate VisitTile: %s", fe.getWhat()); + float score = 0; + const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0); + const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget(); + + bool arriveNextWeek = false; + if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7) + arriveNextWeek = true; + +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d", + priorityTier, + task->toString(), + evaluationContext.armyLossPersentage, + (int)evaluationContext.turn, + evaluationContext.movementCostByRole[HeroRole::MAIN], + evaluationContext.movementCostByRole[HeroRole::SCOUT], + goldRewardPerTurn, + evaluationContext.goldCost, + evaluationContext.armyReward, + evaluationContext.armyGrowth, + evaluationContext.skillReward, + evaluationContext.danger, + evaluationContext.threatTurns, + evaluationContext.threat, + evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", + evaluationContext.strategicalValue, + evaluationContext.conquestValue, + evaluationContext.closestWayRatio, + evaluationContext.enemyHeroDangerRatio, + evaluationContext.explorePriority, + evaluationContext.isDefend); +#endif + + switch (priorityTier) + { + case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach + { + if (evaluationContext.turn > 0) + return 0; + if(evaluationContext.conquestValue > 0) + score = 1000; + if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::INSTADEFEND: //Defend immediately threatened towns + { + if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) + score = evaluationContext.armyInvolvement; + if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score *= evaluationContext.closestWayRatio; + break; + } + case PriorityTier::KILL: //Take towns / kill heroes that are further away + { + if (evaluationContext.turn > 0 && evaluationContext.isHero) + return 0; + if (arriveNextWeek && evaluationContext.isEnemy) + return 0; + if (evaluationContext.conquestValue > 0) + score = 1000; + if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::UPGRADE: + { + if (!evaluationContext.isArmyUpgrade) + return 0; + if (evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::HIGH_PRIO_EXPLORE: + { + if (evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (evaluationContext.explorePriority != 1) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::HUNTER_GATHER: //Collect guarded stuff + { + if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) + return 0; + if (evaluationContext.buildingCost.marketValue() > 0) + return 0; + if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0)) + return 0; + if (evaluationContext.explorePriority == 3) + return 0; + if (evaluationContext.isArmyUpgrade) + return 0; + if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score += evaluationContext.strategicalValue * 1000; + score += evaluationContext.goldReward; + score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; + score += evaluationContext.armyReward; + score += evaluationContext.armyGrowth; + score -= evaluationContext.goldCost; + score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage; + if (score > 0) + { + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + } + break; + } + case PriorityTier::LOW_PRIO_EXPLORE: + { + if (evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (evaluationContext.explorePriority != 3) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::DEFEND: //Defend whatever if nothing else is to do + { + if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange) + return 0; + if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade) + score = 1000; + score *= evaluationContext.closestWayRatio; + score /= (evaluationContext.turn + 1); + break; + } + case PriorityTier::BUILDINGS: //For buildings and buying army + { + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + //If we already have locked resources, we don't look at other buildings + if (ai->getLockedResources().marketValue() > 0) + return 0; + score += evaluationContext.conquestValue * 1000; + score += evaluationContext.strategicalValue * 1000; + score += evaluationContext.goldReward; + score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; + score += evaluationContext.armyReward; + score += evaluationContext.armyGrowth; + if (evaluationContext.buildingCost.marketValue() > 0) + { + if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1) + { + logAi->trace("Should make sure to build market-place instead of %s", task->toString()); + for (auto town : ai->cb->getTownsInfo()) + { + if (!town->hasBuiltSomeTradeBuilding()) + return 0; + } + } + score += 1000; + auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources(); + auto income = ai->buildAnalyzer->getDailyIncome(); + if(ai->buildAnalyzer->isGoldPressureHigh()) + score /= evaluationContext.buildingCost.marketValue(); + if (!resourcesAvailable.canAfford(evaluationContext.buildingCost)) + { + TResources needed = evaluationContext.buildingCost - resourcesAvailable; + needed.positive(); + int turnsTo = needed.maxPurchasableCount(income); + if (turnsTo == INT_MAX) + return 0; + else + score /= turnsTo; + } + } + else + { + if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue)) + return 0; + } + break; + } + } + result = score; + //TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind). + if (std::isnan(result)) + return 0; } #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", + logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f", + priorityTier, task->toString(), evaluationContext.armyLossPersentage, (int)evaluationContext.turn, @@ -1223,9 +1573,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) goldRewardPerTurn, evaluationContext.goldCost, evaluationContext.armyReward, + evaluationContext.armyGrowth, + evaluationContext.skillReward, evaluationContext.danger, + evaluationContext.threatTurns, + evaluationContext.threat, evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.strategicalValue, + evaluationContext.conquestValue, evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, result); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index a0e68d6c6..ee983e43b 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -41,6 +41,7 @@ public: float getResourceRequirementStrength(int resType) const; float getResourceRequirementStrength(const TResources & res) const; float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const; + float getConquestValue(const CGObjectInstance* target) const; float getTotalResourceRequirementStrength(int resType) const; float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const; float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const; @@ -48,7 +49,7 @@ public: uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; uint64_t townArmyGrowth(const CGTownInstance * town) const; - uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const; + float getManaRecoveryArmyReward(const CGHeroInstance * hero) const; }; struct DLL_EXPORT EvaluationContext @@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext int32_t goldCost; float skillReward; float strategicalValue; + float conquestValue; HeroRole heroRole; uint8_t turn; RewardEvaluator evaluator; float enemyHeroDangerRatio; + float threat; + float armyInvolvement; + int defenseValue; + bool isDefend; + int threatTurns; + TResources buildingCost; + bool involvesSailing; + bool isTradeBuilding; + bool isExchange; + bool isArmyUpgrade; + bool isHero; + bool isEnemy; + int explorePriority; EvaluationContext(const Nullkiller * ai); @@ -91,7 +106,20 @@ public: ~PriorityEvaluator(); void initVisitTile(); - float evaluate(Goals::TSubgoal task); + float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS); + + enum PriorityTier : int32_t + { + BUILDINGS = 0, + INSTAKILL, + INSTADEFEND, + KILL, + UPGRADE, + HIGH_PRIO_EXPLORE, + HUNTER_GATHER, + LOW_PRIO_EXPLORE, + DEFEND + }; private: const Nullkiller * ai; diff --git a/AI/Nullkiller/Engine/Settings.cpp b/AI/Nullkiller/Engine/Settings.cpp index db4e3f455..6cc7a5266 100644 --- a/AI/Nullkiller/Engine/Settings.cpp +++ b/AI/Nullkiller/Engine/Settings.cpp @@ -11,6 +11,8 @@ #include #include "Settings.h" + +#include "../../../lib/constants/StringConstants.h" #include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" @@ -22,56 +24,42 @@ namespace NKAI { - Settings::Settings() + Settings::Settings(int difficultyLevel) : maxRoamingHeroes(8), mainHeroTurnDistanceLimit(10), scoutHeroTurnDistanceLimit(5), - maxGoldPressure(0.3f), + maxGoldPressure(0.3f), + retreatThresholdRelative(0.3), + retreatThresholdAbsolute(10000), + safeAttackRatio(1.1), maxpass(10), + pathfinderBucketsCount(1), + pathfinderBucketSize(32), allowObjectGraph(true), useTroopsFromGarrisons(false), - openMap(true) + updateHitmapOnTileReveal(false), + openMap(true), + useFuzzy(false) { - JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings"); + const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyLevel]; + const JsonNode & rootNode = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings"); + const JsonNode & node = rootNode[difficultyName]; - if(node.Struct()["maxRoamingHeroes"].isNumber()) - { - maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer(); - } - - if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber()) - { - mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer(); - } - - if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber()) - { - scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer(); - } - - if(node.Struct()["maxpass"].isNumber()) - { - maxpass = node.Struct()["maxpass"].Integer(); - } - - if(node.Struct()["maxGoldPressure"].isNumber()) - { - maxGoldPressure = node.Struct()["maxGoldPressure"].Float(); - } - - if(!node.Struct()["allowObjectGraph"].isNull()) - { - allowObjectGraph = node.Struct()["allowObjectGraph"].Bool(); - } - - if(!node.Struct()["openMap"].isNull()) - { - openMap = node.Struct()["openMap"].Bool(); - } - - if(!node.Struct()["useTroopsFromGarrisons"].isNull()) - { - useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool(); - } + maxRoamingHeroes = node["maxRoamingHeroes"].Integer(); + mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer(); + scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer(); + maxpass = node["maxpass"].Integer(); + pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer(); + pathfinderBucketSize = node["pathfinderBucketSize"].Integer(); + maxGoldPressure = node["maxGoldPressure"].Float(); + retreatThresholdRelative = node["retreatThresholdRelative"].Float(); + retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float(); + maxArmyLossTarget = node["maxArmyLossTarget"].Float(); + safeAttackRatio = node["safeAttackRatio"].Float(); + allowObjectGraph = node["allowObjectGraph"].Bool(); + updateHitmapOnTileReveal = node["updateHitmapOnTileReveal"].Bool(); + openMap = node["openMap"].Bool(); + useFuzzy = node["useFuzzy"].Bool(); + useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool(); } } diff --git a/AI/Nullkiller/Engine/Settings.h b/AI/Nullkiller/Engine/Settings.h index 775f7f399..ac8b718ec 100644 --- a/AI/Nullkiller/Engine/Settings.h +++ b/AI/Nullkiller/Engine/Settings.h @@ -25,21 +25,37 @@ namespace NKAI int mainHeroTurnDistanceLimit; int scoutHeroTurnDistanceLimit; int maxpass; + int pathfinderBucketsCount; + int pathfinderBucketSize; float maxGoldPressure; + float retreatThresholdRelative; + float retreatThresholdAbsolute; + float safeAttackRatio; + float maxArmyLossTarget; bool allowObjectGraph; bool useTroopsFromGarrisons; + bool updateHitmapOnTileReveal; bool openMap; + bool useFuzzy; public: - Settings(); + explicit Settings(int difficultyLevel); int getMaxPass() const { return maxpass; } float getMaxGoldPressure() const { return maxGoldPressure; } + float getRetreatThresholdRelative() const { return retreatThresholdRelative; } + float getRetreatThresholdAbsolute() const { return retreatThresholdAbsolute; } + float getSafeAttackRatio() const { return safeAttackRatio; } + float getMaxArmyLossTarget() const { return maxArmyLossTarget; } int getMaxRoamingHeroes() const { return maxRoamingHeroes; } int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; } int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; } + int getPathfinderBucketsCount() const { return pathfinderBucketsCount; } + int getPathfinderBucketSize() const { return pathfinderBucketSize; } bool isObjectGraphAllowed() const { return allowObjectGraph; } bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; } + bool isUpdateHitmapOnTileReveal() const { return updateHitmapOnTileReveal; } bool isOpenMap() const { return openMap; } + bool isUseFuzzy() const { return useFuzzy; } }; } diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index b191f96a5..27adf052e 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -104,6 +104,7 @@ namespace Goals bool isAbstract; SETTER(bool, isAbstract) int value; SETTER(int, value) ui64 goldCost; SETTER(ui64, goldCost) + TResources buildingCost; SETTER(TResources, buildingCost) int resID; SETTER(int, resID) int objid; SETTER(int, objid) int aid; SETTER(int, aid) diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.cpp b/AI/Nullkiller/Goals/AdventureSpellCast.cpp index 1868d7c60..8e8df0241 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.cpp +++ b/AI/Nullkiller/Goals/AdventureSpellCast.cpp @@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai) throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated()); } + if (hero->inTownGarrison) + ai->myCb->swapGarrisonHero(hero->visitedTown); + auto wait = cb->waitTillRealize; cb->waitTillRealize = true; diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp index e03901910..5e7f8df63 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp @@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai) if(!town->garrisonHero) { - while(upperArmy->stacksCount() != 0) + if (!garrisonHero->canBeMergedWith(*town)) { - cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first); + while (upperArmy->stacksCount() != 0) + { + cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first); + } } } diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index a12489e99..0391a4585 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance * { hero = path.targetHero; tile = path.targetTile(); + closestWayRatio = 1; if(obj) { @@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai) ai->nullkiller->setActive(chainPath.targetHero, tile); ai->nullkiller->setTargetObject(objid); + ai->nullkiller->objectClusterizer->reset(); auto targetObject = ai->myCb->getObj(static_cast(objid), false); diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index 5b55c55ff..810a6162c 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai) std::unique_lock lockGuard(ai->nullkiller->aiStateMutex); ai->nullkiller->heroManager->update(); + ai->nullkiller->objectClusterizer->reset(); } } diff --git a/AI/Nullkiller/Goals/RecruitHero.h b/AI/Nullkiller/Goals/RecruitHero.h index 101588f19..c49644948 100644 --- a/AI/Nullkiller/Goals/RecruitHero.h +++ b/AI/Nullkiller/Goals/RecruitHero.h @@ -44,6 +44,7 @@ namespace Goals } std::string toString() const override; + const CGHeroInstance* getHero() const override { return heroToBuy; } void accept(AIGateway * ai) override; }; } diff --git a/AI/Nullkiller/Goals/StayAtTown.cpp b/AI/Nullkiller/Goals/StayAtTown.cpp index 346b2c44d..817ddbe1c 100644 --- a/AI/Nullkiller/Goals/StayAtTown.cpp +++ b/AI/Nullkiller/Goals/StayAtTown.cpp @@ -36,16 +36,12 @@ std::string StayAtTown::toString() const { return "Stay at town " + town->getNameTranslated() + " hero " + hero->getNameTranslated() - + ", mana: " + std::to_string(hero->mana); + + ", mana: " + std::to_string(hero->mana) + + " / " + std::to_string(hero->manaLimit()); } void StayAtTown::accept(AIGateway * ai) { - if(hero->visitedTown != town) - { - logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated()); - } - ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE); } diff --git a/AI/Nullkiller/Helpers/ExplorationHelper.cpp b/AI/Nullkiller/Helpers/ExplorationHelper.cpp index 75f40e9cb..0c17e0cc2 100644 --- a/AI/Nullkiller/Helpers/ExplorationHelper.cpp +++ b/AI/Nullkiller/Helpers/ExplorationHelper.cpp @@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile) continue; } - if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger())) + if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio())) { bestGoal = goal; bestValue = ourValue; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index f7807dc5c..a40fbd7d2 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -39,17 +39,17 @@ const uint64_t CHAIN_MAX_DEPTH = 4; const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false; -AISharedStorage::AISharedStorage(int3 sizes) +AISharedStorage::AISharedStorage(int3 sizes, int numChains) { if(!shared){ shared.reset(new boost::multi_array( - boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS])); + boost::extents[sizes.z][sizes.x][sizes.y][numChains])); nodes = shared; foreach_tile_pos([&](const int3 & pos) { - for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++) + for(auto i = 0; i < numChains; i++) { auto & node = get(pos)[i]; @@ -92,8 +92,18 @@ void AIPathNode::addSpecialAction(std::shared_ptr action) } } +int AINodeStorage::getBucketCount() const +{ + return ai->settings->getPathfinderBucketsCount(); +} + +int AINodeStorage::getBucketSize() const +{ + return ai->settings->getPathfinderBucketSize(); +} + AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes) - : sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes) + : sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount()) { accessibility = std::make_unique>( boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]); @@ -169,8 +179,8 @@ std::optional AINodeStorage::getOrCreateNode( const EPathfindingLayer layer, const ChainActor * actor) { - int bucketIndex = ((uintptr_t)actor + static_cast(layer)) % AIPathfinding::BUCKET_COUNT; - int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE; + int bucketIndex = ((uintptr_t)actor + static_cast(layer)) % ai->settings->getPathfinderBucketsCount(); + int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize(); auto chains = nodes.get(pos); if(blocked(pos, layer)) @@ -178,7 +188,7 @@ std::optional AINodeStorage::getOrCreateNode( return std::nullopt; } - for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--) + for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--) { AIPathNode & node = chains[i + bucketOffset]; @@ -486,8 +496,8 @@ public: AINodeStorage & storage, const std::vector & tiles, uint64_t chainMask, int heroChainTurn) :existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles) { - existingChains.reserve(AIPathfinding::NUM_CHAINS); - newChains.reserve(AIPathfinding::NUM_CHAINS); + existingChains.reserve(storage.getBucketCount() * storage.getBucketSize()); + newChains.reserve(storage.getBucketCount() * storage.getBucketSize()); } void execute(const tbb::blocked_range& r) @@ -719,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain( if(node->action == EPathNodeAction::BATTLE || node->action == EPathNodeAction::TELEPORT_BATTLE || node->action == EPathNodeAction::TELEPORT_NORMAL + || node->action == EPathNodeAction::DISEMBARK || node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT) { continue; @@ -961,7 +972,7 @@ void AINodeStorage::setHeroes(std::map heroes) // do not allow our own heroes in garrison to act on map if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison - && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached())) + && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false))) { continue; } @@ -1196,6 +1207,11 @@ void AINodeStorage::calculateTownPortal( continue; } + if (targetTown->visitingHero + && (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID() + || targetTown->getUpperArmy()->stacksCount())) + continue; + auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown); if(nodeOptional) @@ -1418,6 +1434,10 @@ void AINodeStorage::calculateChainInfo(std::vector & paths, const int3 & path.heroArmy = node.actor->creatureSet; path.armyLoss = node.armyLoss; path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle); + for (auto pathNode : path.nodes) + { + path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger); + } if(path.targetObjectDanger > 0) { @@ -1564,7 +1584,7 @@ uint8_t AIPath::turn() const uint64_t AIPath::getHeroStrength() const { - return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy); + return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy); } uint64_t AIPath::getTotalDanger() const diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index a7352acdb..2761cc692 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -29,9 +29,6 @@ namespace NKAI { namespace AIPathfinding { - const int BUCKET_COUNT = 3; - const int BUCKET_SIZE = 7; - const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE; const int CHAIN_MAX_DEPTH = 4; } @@ -157,7 +154,7 @@ public: static boost::mutex locker; static uint32_t version; - AISharedStorage(int3 mapSize); + AISharedStorage(int3 sizes, int numChains); ~AISharedStorage(); STRONG_INLINE @@ -197,6 +194,9 @@ public: bool selectFirstActor(); bool selectNextActor(); + int getBucketCount() const; + int getBucketSize() const; + std::vector getInitialNodes() override; virtual void calculateNeighbours( @@ -298,7 +298,7 @@ public: inline int getBucket(const ChainActor * actor) const { - return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT; + return ((uintptr_t)actor * 395) % getBucketCount(); } void calculateTownPortalTeleportations(std::vector & neighbours); diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index 55a8a735a..4d3a86a28 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t initialMovement = hero->movementPointsRemaining(); initialTurn = 0; armyValue = getHeroArmyStrengthWithCommander(hero, hero); - heroFightingStrength = hero->getFightingStrength(); + heroFightingStrength = hero->getHeroStrength(); tiCache.reset(new TurnInfo(hero)); } diff --git a/AI/VCAI/AIUtility.h b/AI/VCAI/AIUtility.h index 18b6133b4..c1a19d858 100644 --- a/AI/VCAI/AIUtility.h +++ b/AI/VCAI/AIUtility.h @@ -25,11 +25,9 @@ using crstring = const std::string &; using dwellingContent = std::pair>; const int ACTUAL_RESOURCE_COUNT = 7; -const int ALLOWED_ROAMING_HEROES = 8; //implementation-dependent extern const double SAFE_ATTACK_CONSTANT; -extern const int GOLD_RESERVE; extern thread_local CCallback * cb; extern thread_local VCAI * ai; diff --git a/AI/VCAI/ResourceManager.cpp b/AI/VCAI/ResourceManager.cpp index e8f9b75ce..44c0e53d8 100644 --- a/AI/VCAI/ResourceManager.cpp +++ b/AI/VCAI/ResourceManager.cpp @@ -14,8 +14,6 @@ #include "../../CCallback.h" #include "../../lib/mapObjects/MapObjects.h" -#define GOLD_RESERVE (10000); //at least we'll be able to reach capitol - ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal) : resources(Res), goal(Goal) { diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 4e5f41ea2..8af203906 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1314,8 +1314,6 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const return false; if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager return false; - if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES) - return false; if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) return false; if(!cb->getAvailableHeroes(t).size()) diff --git a/CI/example.markdownlint-cli2.jsonc b/CI/example.markdownlint-cli2.jsonc new file mode 100644 index 000000000..a9cfda2cb --- /dev/null +++ b/CI/example.markdownlint-cli2.jsonc @@ -0,0 +1,278 @@ +{ + "config" : { + "default" : true, + + // MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md001.md + "MD001": false, + + // MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md003.md + "MD003": { + "style": "atx" + }, + + // MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md004.md + "MD004": false, + // FIXME: enable and consider fixing + //{ + // "style": "consistent" + //}, + + // MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md005.md + "MD005": true, + + // MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md007.md + "MD007": { + // Spaces for indent + "indent": 2, + // Whether to indent the first level of the list + "start_indented": false, + // Spaces for first level indent (when start_indented is set) + "start_indent": 0 + }, + + // MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md009.md + "MD009": { + // Spaces for line break + "br_spaces": 2, + // Allow spaces for empty lines in list items + "list_item_empty_lines": false, + // Include unnecessary breaks + "strict": false + }, + + // MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md010.md + "MD010": { + // Include code blocks + "code_blocks": false, + // Fenced code languages to ignore + "ignore_code_languages": [], + // Number of spaces for each hard tab + "spaces_per_tab": 4 + }, + + // MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md011.md + "MD011": true, + + // MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md012.md + "MD012": { + // Consecutive blank lines + "maximum": 1 + }, + + // MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md013.md + "MD013": false, + + // MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md014.md + "MD014": true, + + // MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md018.md + "MD018": true, + + // MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md019.md + "MD019": true, + + // MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md020.md + "MD020": true, + + // MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md021.md + "MD021": true, + + // MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md022.md + "MD022": { + // Blank lines above heading + "lines_above": 1, + // Blank lines below heading + "lines_below": 1 + }, + + // MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md023.md + "MD023": true, + + // MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md024.md + "MD024": false, + // FIXME: false positives? + //{ + // // Only check sibling headings + // "allow_different_nesting": true, + // // Only check sibling headings + // "siblings_only": true + //}, + + // MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md025.md + "MD025": { + // Heading level + "level": 1, + // RegExp for matching title in front matter + "front_matter_title": "^\\s*title\\s*[:=]" + }, + + // MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md026.md + "MD026": { + // Punctuation characters + "punctuation": ".,;:!。,;:!" + }, + + // MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md027.md + "MD027": true, + + // MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md028.md + "MD028": true, + + // MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md029.md + "MD029": false, + // FIXME: false positives or broken formatting + //{ + // // List style + // "style": "ordered" + //}, + + // MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md030.md + "MD030": { + // Spaces for single-line unordered list items + "ul_single": 1, + // Spaces for single-line ordered list items + "ol_single": 1, + // Spaces for multi-line unordered list items + "ul_multi": 1, + // Spaces for multi-line ordered list items + "ol_multi": 1 + }, + + // MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md031.md + "MD031": { + // Include list items + "list_items": false + }, + + // MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md032.md + "MD032": true, + + // MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md + "MD033": false, + // FIXME: enable and consider fixing + //{ + // // Allowed elements + // "allowed_elements": [] + //}, + + // MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md034.md + "MD034": true, + + // MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md035.md + "MD035": { + // Horizontal rule style + "style": "consistent" + }, + + // MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md + "MD036": false, + // FIXME: enable and consider fixing + // { + // // Punctuation characters + // "punctuation": ".,;:!?。,;:!?" + // }, + + // MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md037.md + "MD037": true, + + // MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md038.md + "MD038": true, + + // MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md039.md + "MD039": true, + + // MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md040.md + "MD040": { + // List of languages + "allowed_languages": [ "cpp", "json", "sh", "text", "nix", "powershell", "lua" ], + // Require language only + "language_only": true + }, + + // MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md041.md + "MD041": { + // Heading level + "level": 1, + // RegExp for matching title in front matter + "front_matter_title": "^\\s*title\\s*[:=]" + }, + + // MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md042.md + "MD042": true, + + // MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md043.md + "MD043": false, + + // MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md044.md + "MD044": false, + + // MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md045.md + "MD045": false, + + // MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md046.md + "MD046": { + // Block style + "style": "fenced" + }, + + // MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md047.md + "MD047": true, + + // MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md048.md + "MD048": { + // Code fence style + "style": "backtick" + }, + + // MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md049.md + "MD049": { + // Emphasis style + "style": "asterisk" + }, + + // MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md050.md + "MD050": { + // Strong style + "style": "asterisk" + }, + + + + // MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md051.md + "MD051": true, + + // MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md052.md + "MD052": { + // Include shortcut syntax + "shortcut_syntax": false + }, + + // MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md053.md + "MD053": { + // Ignored definitions + "ignored_definitions": [ + "//" + ] + }, + + // MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md054.md + "MD054": { + // Allow autolinks + "autolink": true, + // Allow inline links and images + "inline": true, + // Allow full reference links and images + "full": true, + // Allow collapsed reference links and images + "collapsed": true, + // Allow shortcut reference links and images + "shortcut": true, + // Allow URLs as inline links + "url_inline": true + }, + + // MD058 - Tables should be surrounded by blank lines + "MD058" : true + + } +} \ No newline at end of file diff --git a/ChangeLog.md b/ChangeLog.md index 85e62bc9d..f499ca1ad 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,23 +1,32 @@ -# 1.5.7 -> 1.6.0 (in development) +# VCMI Project Changelog + +## 1.5.7 -> 1.6.0 (in development) ### Major changes + +* Greatly improved decision-making of NullkillerAI * Implemented handicap system, with options to reduce income and growth in addition to starting resources restriction * Game will now show statistics after scenario completion, such as resources or army strength over time * Implemented spell quick selection panel in combat * Implemented adventure map overlay accessible via Alt key that highlights all interactive objects on screen * Implemented xBRZ upscaling filter +* Added support for high-resolution graphical assets * It is now possible to import data from Heroes Chronicles (gog.com installer only) as custom campaigns * Added simple support for spell research feature from HotA that can be enabled via mod or game configuration editing * Implemented automatic selection of interface scaling. Selecting interface scaling manually will restore old behavior * VCMI will now launch in fullscreen on desktop systems. Use F4 hotkey or toggle option in settings to restore old behavior ### General + * Saved game size reduced by approximately 3 times, especially for large maps or games with a large number of mods. * Mods that modify game texts, such as descriptions of secondary skills, will now correctly override translation mods * Game will now correctly restore information such as hero path, order of heroes and towns, and list of sleeping heroes on loading a save game * Added translation for missing texts, such as random map descriptions, quick exchange buttons, wog commander abilities, moat names +* When playing in non-English language using English Heroes III data files, game will now load all maps and campaigns using player language +* Added `vcmiscrolls` cheat code that gives spell scrolls for every possible spells ### Multiplayer + * Added option to start vcmi server on randomly selected TCP port * Fixed potential desynchronization between server and clients on randomization of map objects if client and server run on different operating systems * Fixed possible freeze on receiving turn in multiplayer when player has town window opened @@ -27,11 +36,15 @@ * Game will now correctly show turn timers and simultaneous turns state on loading game ### Stability + * Fixed possible crash on connecting bluetooth mouse during gameplay on Android * VCMI will now write more detailed information to log file on crash due to uncaught exception * Fixed crash on transfer of multiple artifacts in a backpack to another hero on starting next campaign scenario without hero that held these artifacts before +* Fixed crash on dismissing hero after picking up an artifact from hero doll +* Fixed possible crash if creature with spell before attack bonus kills unit it was attacking with spell ### Mechanics + * Arrow tower will now prefer to attack more units that are viewed most dangerous instead of simply attacking top-most unit * Score in campaigns will now be correctly tracked for games loaded from a save * Fixed incorrect direction of Dragon Breath attack in some cases if wide creature attacks another wide creature @@ -47,8 +60,15 @@ * Chain Lightning will now skip over creatures that are immune to this spell instead of targeting them but dealing no damage * Commanders spell resistance now uses golem-like logic which reduces damage instead of using dwarf-style change to block spell * It is now possible to target empty hex for shooters with area attack, such as Magog or Lich +* View Earth will no longer reveal position of enemy heroes and towns +* It is now possible to sell Grail, in line with Heroes III +* Jeddite is no longer female +* Mutare and Mutare Drake are now Overlord and not Warlock +* Elixir of Life no longer affects siege machines +* Banned skills known by hero now have minimal chance (1) instead of 0 to appear on levelup ### Video / Audio + * Fixed playback of audio stream with different formats from video files in some Heroes 3 versions * Video playback will not be replaced by a black square when another dialogue box is on top of the video. * When resuming video playback, the video will now be continued instead of being restarted. @@ -64,21 +84,29 @@ * Fixed computation of audio length for formats other than .wav. This fixes incorrect text scrolling speed in campaign intro/outro * Game will now use Noto family true type font to display characters not preset in Heroes III fonts * Added option to scale all in-game fonts when scalable true type fonts are in use +* Some of the assets provided by VCMI are now available in higher resolution +* Implemented support for semi-transparent spell effects, such as Life Drain or Resurrection (and many others) ### Interface + * It is now possible to search for a map object using Ctrl+F hotkey +* Holding Alt while using move unit button on Exchange screen will now move entire army except for single unit in this slot * Added option to drag map with right-click * Added hotkeys to reorder list of owned towns or heroes * The number of units resurrected using the Life Drain ability is now written to the combat log. * Fixed order of popup dialogs after battle. +* Creature window now displays source of all bonuses that creature has, such as creature ability, hero skill, hero artifact, etc. +* Reduced font used for creature abilities description to reduce text clipping * Right-click on wandering monster on adventure map will now also show creature level and faction it belongs to * Added additional information to map right-click popup dialog: map author, map creation date, map version * Added scrollbars for selection of starting town, starting hero, and tavern invite if number of objects is too large to fit into the screen * Fixed incorrect battle turn queue displaying incorrect turn order when all units have waited * Semi-transparent shadows now correctly update their transparency during fading effects, such as resource pickups +* Fixed swapped Overlord and Warlock models on adventure map +* Fixed Heroes III bug - swapped icons of View Earth and View Air * Game will now save all names for human player in hotseat mode * Added unassigned by default shortcuts for toggling visibility of visitable and blocked tiles -* Spellbook button in battle is now blocked if hero has no spellbook +* Spellbook button in battle is now blocked if hero has no spellbook * Adventure map will no longer scroll if window is not in focus * Removed second info window when player loses his last town * Fixed hero path not updating correctly after hiring or dismissing creatures @@ -91,45 +119,82 @@ * Quick backpack window is now also accessible via Shift+mouse click, similar to HD Mod * It is now possible to sort artifacts in backpack by cost, slot, or rarity class * Fixed incorrect display of names of VCMI maps in scenario selection if multiple VCMI map are present in list +* Fixed bug leading to inability to select larger number of "CPU only players" in random map generation menu +* It is now possible to delete saved games +* Game will now promt to delete saves from no longer supported versions of VCMI +* It is now possible to use scroll in touch popup windows +* Name of spell provided by Shrine is now displayed in yellow color +* Fixed right-click popup on hero in town placed outside of screen boundaries on low resolutions +* Fixed misaligned button in 2-player alliance selector in random map generation menu +* Damage range of Ballista in unit window now accounts for hero attack skill, in line with Heroes III +* Changed format of automatic autosave to more human-readable version +* Names of autosave folders are now left-aligned in save game list ### Random Maps Generator + * Implemented connection option 'forcePortal' * It is now possible to connect zone to itself using pair of portals * It is now possible for a random map template to change game settings * Road settings will now be correctly loaded when opening random map setup tab +* Roads placed on the maps will now be curved a little bit to improve the look of the maps. * Added support for banning objects per zones * Added support for customizing objects frequency, value, and count per zone * Fixed values of Pandora Boxes with creatures to be in line with H3:SoD +* Added sealed zone types, for entirely unpassable zones. +* It is now possible to connect two zones with multiple connections of same or different types ### Campaigns + * It is now possible to use .zip archive for VCMI campaigns instead of raw gzip stream * Fixed handling of hero placeholders in VCMI map format (.vmap) * Fixed not functioning hero carryover in VCMI campaigns * Added support for campaign outro videos, such as outro in "Song for the Father" campaign * Added support for rim image for intro video, such as opening videos in Heroes Chronicles * Added support for custom loading screen in campaigns -* Added support for custom region definitions (such as background images) for VCMI campaigns +* Added support for custom region definitions (such as background images) for VCMI campaigns -### AI -* VCMI will now use BattleAI for battles with neutral enemies by default -* Fixed bug where BattleAI attempts to move double-wide unit to an unreachable hex +### Adventure AI + +* Greatly improved decision-making of NullkillerAI +* NullkillerAI will now act differently based on difficulty level * Fixed several cases where Nullkiller AI can count same dangerous object twice, doubling expected army loss. * Nullkiller is now capable of visiting configurable objects from mods * Nullkiller now uses whirlpools for map movement +* Fixed possible crash on AI attempting to visit town that is already being visited by this hero +* It is now possible to configure NullkillerAI parameters separately for each game difficulty +* Extended hardcoded logic of AI not taking creatures from Garrisons to all Heroes III: Restoration of Erathia campaigns, in line with original game + +### Combat AI + +* VCMI will now use BattleAI for battles with neutral enemies by default +* Fixed bug where BattleAI attempts to move double-wide unit to an unreachable hex +* Fixed a bug causing BattleAI to focus on unreachable targets and ignoring reachable enemies * AI can now correctly estimate effect of Dragon Breath and other similar abilities * Battle AI should now avoid ending turn on the moat * Fixed case where BattleAI will go around the map to attack ranged units if direct path is blocked by another unit * Fixed evaluation of effects of waiting if unit is under haste effect by Battle AI * Battle AI can now use location spells * Battle AI will now correctly avoid moving flying units into dangerous obstacles such as moat -* Fixed possible crash on AI attempting to visit town that is already being visited by this hero ### Launcher + +* It is now always possible to disable or uninstall a mod. Any mods that depend on this mod will be automatically disabled +* It is now always possible to update a mod, even if there are mods that depend on this mod. +* It is now possible to enable mod that conflicts with already active mod. Conflicting mods will be automatically disabled +* If main mod is disabled, all its submods will have their active or inactive status shown as greyed-out for clarity +* If mod depends or conflicts with a submod, Launcher will now also show name of parent mod in list of dependencies / conflicts +* Game will now cache result of mod repository checkout and restore it immediately on next start. This removes flickering when game fills list of available mods. +* Screenshot and Changelog tabs in mod description are now disabled for mods that do not have them. +* Launcher will now correctly show conflicts on both sides - if mod A is marked as conflicting with B, then information on this conflict will be shown in description of both mod A and mod B (instead of only in mod B) * Added Swedish translation +* Added better diagnostics for gog installer extraction errors ### Map Editor + +* It is now possible to remove any map object as part of timed event * Implemented tracking of building requirements for Building Dialog * Added build/demolish/enable/disable all buildings options to Building Dialog in town properties +* Added support for customization of heroes artifacts * Implemented configuration of patrol radius for heroes * It is now possible to set spells allowed or required to be present in Mages Guild * It is now possible to add timed events to a town @@ -141,8 +206,13 @@ * Removed separate versioning of map editor. Map editor now has same version as VCMI * Timed events interfaces now counts days from 1, instead of from 0 * Added Recent Files to File Menu and Toolbar +* Fixed crash on attempting to save map with random dwelling ### Modding + +* Json configuration files from different mods no longer can override each other to reduce possibility of a file name clash +* Game will now load high-resolution assets when xbrz upscaler is in use from Data2x, Data3x, Data4x, or Sprites2x, Sprites3x, Sprites4x directories. +* Game will now load high-resolution movies when xbrz upscaler is in use from Video2x, Video3x, Video4x directories * Added support for configurable flaggable objects that can provide bonuses or daily income to owning player * Added support for soft dependencies for mods, that only affect mod loading order (and as result - override order), without requiring dependent mod or allowing access to its identifiers * It is now possible to provide translations for mods that modify strings from original game, such as secondary skill descriptions @@ -154,6 +224,7 @@ * Added support for explicitly visitable town buildings that will activate only on click and not on construction or on hero visit (Mana Vortex from HotA) * It is now possible to add guards to a configurable objects. All H3 creature banks are now implemented as configurable object. * It is now possible to define starting position of units in a guarded configurable object +* It is now possible to add description to an object with "market" handler * Added `canCastWithoutSkip` parameter to a spell. If such spell is cast by a creature, its turn will not end after a spellcast * Added `castOnlyOnSelf` parameter to a spell. Creature that can cast this spell can only cast it on themselves * Mod can now provide pregenerated assets in place of autogenerated, such as large spellbook. @@ -162,12 +233,15 @@ * Added support for multiple music tracks for towns * Added support for multiple music tracks for terrains on adventure map * Fixed several cases where vcmi will report errors in json without specifying filename of invalid file +* Fixed selection of gendered sprites for heroes on adventure map * It is now possible to change minimal values of primary skills for heroes * Added support for HotA Bank building from Factory * Added support for HotA Grotto buiding from Cove * Added support for HotA-style 8th creature in town * Town building can now define war machine produced in this building (Blacksmith or Ballista Yard) * Town building can now define provided fortifications - health of walls, towers, presence of moat, identifier of creature shooter on tower +* War Machines Factory no longer unconditionally contain war machines from the original game, allowing mods to define list of war machines from scratch +* Added MECHANICAL bonus * Added DISINTEGRATE bonus * Added INVINCIBLE bonus * Added PRISM_HEX_ATTACK_BREATH bonus @@ -176,8 +250,11 @@ * Black market restock period setting now correctly restocks on specified date instead of restocking on all dates other than specified one * Json Validator will now attempt to detect typos when encountering unknown property in Json * Added `translate missing` command that will export only untranslated strings into `translationsMissing` directory, separated per mod +* Added support for text subtitiles for video files +* Added validation of objects with "market" and "flaggable" handlers +* Added "special" property for secondary skills -# 1.5.6 -> 1.5.7 +## 1.5.6 -> 1.5.7 * Fixed game freeze if player is attacked in online multiplayer game by another player when he has unread dialogs, such as new week notification * Fixed possible game crash after being attacked by enemy with artifact that blocks spellcasting @@ -189,14 +266,16 @@ * Fixed excessive removal of open dialogs such as new week or map events on new turn * Fixed objects like Mystical Gardens not resetting their state on new week correctly -# 1.5.5 -> 1.5.6 +## 1.5.5 -> 1.5.6 ### Stability + * Fixed possible crash on transferring hero to next campaign scenario if hero has combined artifact some components of which can be transferred * Fixed possible crash on transferring hero to next campaign scenario that has creature with faction limiter in his army * Fixed possible crash on application shutdown due to incorrect destruction order of UI entities ### Multiplayer + * Mod compatibility issues when joining a lobby room now use color coding to make them less easy to miss. * Incompatible mods are now placed before compatible mods when joining lobby room. * Fixed text overflow in online lobby interface @@ -204,21 +283,24 @@ * Fixed non-functioning slider in invite to game room dialog ### Interface + * Fixed some shortcuts that were not active during the enemy's turn, such as Thieves' Guild. * Game now correctly uses melee damage calculation when forcing a melee attack with a shooter. * Game will now close all open dialogs on start of our turn, to avoid bugs like locked right-click popups ### Map Objects + * Spells the hero can't learn are no longer hidden when received from a rewardable object, such as the Pandora Box * Spells that cannot be learned are now displayed with gray text in the name of the spell. * Configurable objects with scouted state such as Witch Hut in HotA now correctly show their reward on right click after vising them but refusing to accept reward * Right-click tooltip on map dwelling now always shows produced creatures. Player that owns the dwelling can also see number of creatures available for recruit ### Modding + * Fixed possible crash on invalid SPELL_LIKE_ATTACK bonus * Added compatibility check when loading maps with old names for boats -# 1.5.4 -> 1.5.5 +## 1.5.4 -> 1.5.5 * Fixed crash when advancing to the next scenario in campaigns when the hero not transferring has a combination artefact that can be transferred to the next scenario. * Fixed game not updating information such as hero path and current music on new day @@ -228,20 +310,23 @@ * Shift+left click now directly opens the hero window when two heroes are in town * Fixed handling of alternative actions for creatures that have more than two potential actions, such as move, shoot, and cast spells. -# 1.5.3 -> 1.5.4 +## 1.5.3 -> 1.5.4 ### Stability + * Fixed a possible crash when clicking on an adventure map when another player is taking a turn in multiplayer mode. * Failure to extract a mod will now display an error message instead of a silent crash. * Fixed crash on opening town hall screen of a town from a mod with invalid building identifier * Fixed crash when faerie dragons die after casting Ice Ring on themselves. ### Mechanics + * The scholar will now correctly upgrade a skill if the visiting hero has offered a skill at either the basic or advanced level. * Hero now reveals Fog of War when receiving new or upgraded secondary skills (such as scouting). * AI will now always act after all human players during simturns instead of acting after host player ### Interface + * Pressing the up and down keys on the town screen will now move to the next or previous town instead of scrolling through the list of towns. * Long text in scenario name and highscore screen now shortened to fit the interface * Game now moves cursor to tap event position when using software cursor with touch screen input @@ -249,16 +334,19 @@ * Damage estimation tooltip will no longer show damage greater than the targeted unit's health. ### Random Maps Generator + * Generator will try to place roads even further away from zone borders * Fixed rare crash when placing two quest artefacts in the same location at the same time ### AI + * Improved performance of Nullkiller AI * Stupid AI no longer overestimates damage when killing entire unit * Fixed a bug leading to Battle AI not using spells when sieging town with Citadel or Castle built * Fixed an unsigned integer overflow that caused the Nullkiller AI to overestimate the total army strength after merging two armies. ### Launcher + * Added button to reset touchscreen tutorial on mobile systems * Launcher will now warn if player selects Gog Galaxy installer instead of offline installer * Launcher will now ask for the .bin file first as it is usually listed first in the file system view @@ -268,16 +356,19 @@ * Fixed manual file installation on Android ### Map Editor + * Icons and translations now embedded in executable file ### Modding + * Improved bonus format validation * Validator now reports valid values for enumeration fields * Fixed missing addInfo field for bonuses that use the BONUS_OWNER_UPDATER propagation updater. -# 1.5.2 -> 1.5.3 +## 1.5.2 -> 1.5.3 ### Stability + * Fixed possible crash when hero class has no valid commander. * Fixed crash when pressing spacebar or enter during combat when hero has no tactics skill. * Fixed crash when receiving a commander level-up after winning a battle in a garrison owned by an enemy player. @@ -290,8 +381,9 @@ * Game will now display an error message instead of silent crash if game data directory is not accessible ### Mechanics + * Transport Artefact victory condition will no longer trigger if another player has completed it. -* Fixed wandering monster combat not triggering when landing in its zone of control when flying from above the monster using the Fly spell. +* Fixed wandering monster combat not triggering when landing in its zone of control when flying from above the monster using the Fly spell. * Fixed potentially infinite movement loop when the hero has Admiral's Hat whirlpool immunity and the hero tries to enter and exit the same whirlpool. * If game picks gold for a random resource pile that has predetermined by map amount, its amount will be correctly multiplied by 100 * Fixed hero not being able to learn spells from a mod in some cases, even if they are available from the town's mage guild. @@ -300,6 +392,7 @@ * If turn timer runs out during pve battle game will end player turn after a battle instead of forcing retreat ### Interface + * Fixed reversed button functions in Exchange Window * Fixed allied towns being missing from the list when using the advanced or expert Town Portal spell. * Fixed corrupted UI that could appear for a frame under certain conditions @@ -312,6 +405,7 @@ * It is now possible to scroll through artifacts backpack using mouse wheel or swipe ### Launcher + * Android now uses the same Qt-based launcher as other systems * Fixed attempt to install a submod when installing new mod that depends on a submod of another mod * Fixed wrong order of activating mods in chain when installing multiple mods at once @@ -325,15 +419,16 @@ * Replaced checkboxes with toggle buttons for easier of access on touchscreens. * Icons and translations now embedded in executable file * Added interface for configuring several previously existing but inaccessible options in Launcher: - * Selection of input tolerance precision for all input types - * Relative cursor mode for mobile systems (was only available on Android) - * Haptic feedback toggle for mobile systems (was only available on Android) - * Sound and music volume (was only available in game) - * Selection of long touch interval (was only available in game) - * Selection of upscaling filter used by SDL - * Controller input sensitivity and acceleration. + * Selection of input tolerance precision for all input types + * Relative cursor mode for mobile systems (was only available on Android) + * Haptic feedback toggle for mobile systems (was only available on Android) + * Sound and music volume (was only available in game) + * Selection of long touch interval (was only available in game) + * Selection of upscaling filter used by SDL + * Controller input sensitivity and acceleration. ### AI + * Fixed crash when Nullkiller AI tries to explore after losing the hero in combat. * Fixed rare crash when Nullkiller AI tries to use portals * Fixed potential crash when Nullkiller AI has access to Town Portal spell @@ -342,19 +437,23 @@ * Fixed bug leading to Battle AI doing nothing if targeted unit is unreachable ### Random Maps Generator + * Fixed crash when player selects a random number of players and selects a different colour to play, resulting in a non-continuous list of players. * Fixed rare crash when generating maps with water ### Map Editor + * Fixed crash on closing map editor ### Modding + * Added new building type 'thievesGuild' which implements HotA building in Cove. * Creature terrain limiter now actually accepts terrain as parameter -# 1.5.1 -> 1.5.2 +## 1.5.1 -> 1.5.2 ### Stability + * Fixed crash on closing game while combat or map animations are playing * Fixed crash on closing game while network thread is waiting for dialog to be closed * Fixed random crash on starting random map with 'random' number of players @@ -368,6 +467,7 @@ * Game will now abort loading if a corrupt mod is detected instead of crashing without explanation later ### Multiplayer + * Contact between allied players will no longer break simturns * Having hero in range of object owned by another player will now be registered as contact * Multiplayer saves are now visible when starting a single player game @@ -376,11 +476,13 @@ * All multiplayer chat commands now use a leading exclamation mark ### Campaigns + * If the hero attacks an enemy player and is defeated, he will be correctly registered as defeated by the defending player. * Allow standard victory condition on 'To kill a hero' campaign mission in line with H3 * Fixes Adrienne starting without Inferno spell in campaign ### Interface + * For artefacts that are part of a combined artefact, the game will now show which component of that artefact your hero has. * Fixed broken in 1.5.1 shortcut for artifact sets saving * Fixed full screen toggle (F4) not applying changes immediately @@ -392,37 +494,38 @@ * Added keyboard shortcuts to markets and altars. 'Space' to confirm deal and 'M' to trade maximum possible amount * Pressing 'Escape' in main menu will now trigger 'Back' and 'Quit' buttons * Added keyboard shortcuts to hero exchange window: - * 'F10' will now swap armies - * 'F11' will now swap artifacts. Additionally, 'Ctrl+F11' will swap equipped artifacts, and 'Shift+F11' will swap backpacks - * Added unassigned shortcuts to move armies or artifacts to left or right side + * 'F10' will now swap armies + * 'F11' will now swap artifacts. Additionally, 'Ctrl+F11' will swap equipped artifacts, and 'Shift+F11' will swap backpacks + * Added unassigned shortcuts to move armies or artifacts to left or right side * Added keyboard shortcuts to access buildings from town interface: - * 'F' will now open Fort window - * 'B' will now open Town Hall window - * 'G' will now open Mage Guild window - * 'M' will now open Marketplace - * 'R' will now open recruitment interface - * 'T' will now open Tavern window - * 'G' will now open Thieves Guild - * 'E' will now open hero exchange screen, if both heroes are present in town - * 'H' will now open hero screen. Additionally, 'Shift+H' will open garrisoned hero screen, and 'Ctrl+H' will open visiting hero screen - * 'Space' will now swap visiting and garrisoned heroes + * 'F' will now open Fort window + * 'B' will now open Town Hall window + * 'G' will now open Mage Guild window + * 'M' will now open Marketplace + * 'R' will now open recruitment interface + * 'T' will now open Tavern window + * 'G' will now open Thieves Guild + * 'E' will now open hero exchange screen, if both heroes are present in town + * 'H' will now open hero screen. Additionally, 'Shift+H' will open garrisoned hero screen, and 'Ctrl+H' will open visiting hero screen + * 'Space' will now swap visiting and garrisoned heroes * Added keyboard shortcuts to switch between tabs in Scenario Selection window: - * 'E' will open Extra Options tab - * 'T' will open Turn Options tab - * 'I' will open Invite Players window (only for lobby games) - * 'R' will now replay video in campaigns + * 'E' will open Extra Options tab + * 'T' will open Turn Options tab + * 'I' will open Invite Players window (only for lobby games) + * 'R' will now replay video in campaigns * Added keyboard shortcuts to Adventure map: - * 'Ctrl+L' will now prompt to open Load Game screen - * 'Ctrl+M' will now prompt to go to main menu - * 'Ctrl+N' will now prompt to go to New Game screen - * 'Ctrl+Q' will now prompt to quit game - * Page Up, Page Down, Home and End keys will now move hero on adventure map similar to numpad equivalents - * Fixed non-functioning shortcuts '+' and '-' on numpad to zoom adventure map + * 'Ctrl+L' will now prompt to open Load Game screen + * 'Ctrl+M' will now prompt to go to main menu + * 'Ctrl+N' will now prompt to go to New Game screen + * 'Ctrl+Q' will now prompt to quit game + * Page Up, Page Down, Home and End keys will now move hero on adventure map similar to numpad equivalents + * Fixed non-functioning shortcuts '+' and '-' on numpad to zoom adventure map * Added keyboard shortcuts to Battle interface: - * 'V' now allows to view information of hovered unit - * 'I' now allows to view information of active unit + * 'V' now allows to view information of hovered unit + * 'I' now allows to view information of active unit ### Mechanics + * Game will no longer pick creatures exclusive to AB campaigns for random creatures or for Refugee Camp, in line with H3 * If original movement rules are on, it is not possible to attack guards from visitable object directly, only from free tile * Fixed bug leading that allowed picking up objects while flying on top of water @@ -430,24 +533,29 @@ * Interface will now use same arrow for U-turns in path as H3 ### AI + * Nullkiller AI can now explore the map * Nullkiller AI will no longer use the map reveal cheat when allied with a human or when playing on low difficulty * Nullkiller AI is now used by default for allied players ### Launcher + * When extracting data from gog.com offline installer game will extract files directly into used data directory instead of temporary directory ### Map Editor + * Fixed victory / loss conditions widget initialization ### Modding + * Hero specialties with multiple bonuses that have TIMES_HERO_LEVEL updater now work as expected * Spells that apply multiple bonuses with same type and subtype but different value type now work as expected * Added option to toggle layout of guards in creature banks -# 1.5.0 -> 1.5.1 +## 1.5.0 -> 1.5.1 ### Stability + * Fixed possible crash on accessing faction description * Fixed possible thread race on exit to main menu * Game will now show error message instead of silent crash on corrupted H3 data @@ -460,6 +568,7 @@ * If json file specified in mod.json is missing, vcmi will now only log an error instead of crashing ### Interface + * Added retaliation damage and kills preview when hovering over units that can be attacked in melee during combat * Clicking on combat log would now open a window with full combat log history * Removed message length limit in text input fields, such as global lobby chat @@ -473,10 +582,12 @@ * Small windows no longer dim the entire screen by default ### Mechanics + * Recruiting a hero will now immediately reveal the fog of war around him * When both a visiting hero and a garrisoned hero are in town, the garrisoned hero will visit town buildings first. ### Multiplayer + * Fixed in-game chat text not being visible after switching from achannel with a long history * Fixed lag when switching to channel with long history * Game now automatically scrolls in-game chat on new messages @@ -491,23 +602,27 @@ * Fixed overflow in invite window when there are more than 8 players in the lobby ### Random Maps Generator + * Generator will now prefer to place roads away from zone borders ### AI + * Fixed possible crash when Nullkiller AI tries to upgrade army * Nullkiller AI will now recruit new heroes if he left with 0 heroes * AI in combat now knows when an enemy unit has used all of its retaliations. ### Map Editor + * Fixed setting up hero types of heroes in Prisons placed in map editor * Fixed crash on setting up Seer Hut in map editor * Added text auto-completion hints for army widget * Editor will now automatically add .vmap extensions when saving map * Fixed text size in map validation window -# 1.4.5 -> 1.5.0 +## 1.4.5 -> 1.5.0 ### General + * Added Portuguese (Brazilian) translation * Added basic support for game controllers * Added option to disable cheats in game @@ -516,6 +631,7 @@ * Implemented switchable artifact sets from HD Mod ### Stability + * Fixed possible crash in Altar of Sacrifice * Fixed possible crash on activation of 'Enchanted' bonus * Fixed possible race condition on random maps generation on placement treasures near border with water zone @@ -532,6 +648,7 @@ * Fixed possible hanging app on attempt to close game during loading ### Multiplayer + * Game map will no longer be locked during turn of other human players, allowing to change hero paths or inspect towns or heroes * Game will now correctly block most of player actions outside of their turn * Implemented new lobby, available in game with persistent accounts and chat @@ -544,6 +661,7 @@ * Implemented rolling and banning of towns before game start ### Interface + * Implemented configurable keyboard shortcuts, editable in file config/shortcutsConfig.json * Fixed broken keyboard shortcuts in main menu * If UI Enhancements are enabled, the game will skip confirmation dialogs when entering owned dwellings or refugee camp. @@ -581,6 +699,7 @@ * Recruitment costs that consist from 3 different resources should now fit recruitment window UI better ### Campaigns + * Game will now correctly track who defeated the hero or wandering monsters for related quests and victory conditions * Birth of a Barbarian: Yog will now start the third scenario with Angelic Alliance in his inventory * Birth of a Barbarian: Heroes with Angelic Alliance components are now considered to be mission-critical and can't be dismissed or lost in combat @@ -598,6 +717,7 @@ * Fixed invalid string on right-clicking secondary skill starting bonus ### Battles + * Added option to enable unlimited combat replays during game setup * Added option to instantly end battle using quick combat (shortcut: 'e') * Added option to replace auto-combat button action with instant end using quick combat @@ -606,6 +726,7 @@ * Fixed positioning of unit stack size label ### Mechanics + * It is no longer possible to learn spells from Pandora or events if hero can not learn them * Fixed behavior of 'Dimension Door' spell to be in line with H3:SoD * Fixed behavior of 'Fly' spell to be in line with H3:SoD @@ -628,6 +749,7 @@ * Fixed regression leading to large elemental dwellings being used as replacements for random dwellings ### Random Maps Generator + * Game will now save last used RMG settings in game and in editor * Reduced number of obstacles placed in water zones * Treasure values in water zone should now be similar to values from HotA, due to bugs in H3:SoD values @@ -644,6 +766,7 @@ * Windmill will now appear on top of all other objects ### Launcher + * Launcher now supports installation of Heroes 3 data using gog.com offline installer thanks to innoextract tool * Fixed loading of mod screenshots if player opens screenshots tab without any preloaded screenshots * Fixed installation of mods if it has non-installed submod as dependency @@ -653,6 +776,7 @@ * Added Portuguese translation to launcher ### Map Editor + * Added Chinese translation to map editor * Added Portuguese translation to map editor * Mod list in settings will now correctly show submods of submods @@ -662,6 +786,7 @@ * It is now possible to customize hero spells ### AI + * Fixed possible crash on updating NKAI pathfinding data * Fixed possible crash if hero has only commander left without army * Fixed possible crash on attempt to build tavern in a town @@ -672,15 +797,16 @@ * It is now possible to configure AI settings via config file * Improved parallelization when AI has multiple heroes * AI-controlled creatures will now correctly move across wide moat in Fortress -* Fixed system error messages caused by visitation of Trading Posts by VCAI +* Fixed system error messages caused by visitation of Trading Posts by VCAI * Patrolling heroes will never retreat from the battle * AI will now consider strength of town garrison and not just strength of visiting hero when deciding to attack town ### Modding + * Added new game setting that allows inviting heroes to taverns * It is now possible to add creature or faction description accessible via right-click of the icon * Fixed reversed Overlord and Warlock classes mapping -* Added 'selectAll' mode for configurable objects which grants all potential rewards +* Added 'selectAll' mode for configurable objects which grants all potential rewards * It is now possible to use most of json5 format in vcmi json files * Main mod.json file (including any submods) now requires strict json, without comments or extra commas * Replaced bonus MANA_PER_KNOWLEDGE with MANA_PER_KNOWLEDGE_PERCENTAGE to avoid rounding error with mysticism @@ -689,9 +815,10 @@ * Game will now report cases where minimal damage of a creature is greater than maximal damage * Added bonuses RESOURCES_CONSTANT_BOOST and RESOURCES_TOWN_MULTIPLYING_BOOST -# 1.4.4 -> 1.4.5 +## 1.4.4 -> 1.4.5 ### Stability + * Fixed crash on creature spellcasting * Fixed crash on unit entering magical obstacles such as quicksands * Fixed freeze on map loading on some systems @@ -699,24 +826,29 @@ * Fixed crash on opening creature information window with invalid SPELL_IMMUNITY bonus ### Random Maps Generator + * Fixed placement of guards sometimes resulting into open connection into third zone * Fixed rare crash on multithreaded access during placement of artifacts or wandering monsters ### Map Editor + * Fixed inspector using wrong editor for some values ### AI -* Fixed bug leading to AI not attacking wandering monsters in some cases -* Fixed crash on using StupidAI for autocombat or for enemy players -# 1.4.3 -> 1.4.4 +* Fixed bug leading to AI not attacking wandering monsters in some cases +* Fixed crash on using StupidAI for autocombat or for enemy players + +## 1.4.3 -> 1.4.4 ### General + * Fixed crash on generation of random maps -# 1.4.2 -> 1.4.3 +## 1.4.2 -> 1.4.3 ### General + * Fixed the synchronisation of the audio and video of the opening movies. * Fixed a bug that caused spells from mods to not show up in the Mage's Guild. * Changed the default SDL driver on Windows from opengl to autodetection @@ -724,6 +856,7 @@ * Movement and mana points are now replenished for new heroes in taverns. ### Multiplayer + * Simturn contact detection will now correctly check for hero moving range * Simturn contact detection will now ignore wandering monsters * Right-clicking the Simturns AI option now displays a tooltip @@ -733,12 +866,14 @@ * Ending a turn during simturns will now block the interface correctly. ### Campaigns + * Player will no longer start the United Front of Song for the Father campaign with two Nimbuses. * Fixed missing campaign description after loading saved game * Campaign completion checkmarks will now be displayed after the entire campaign has been completed, rather than just after the first scenario. * Fixed positioning of prologue and epilogue text during campaign scenario intros ### Interface + * Added an option to hide adventure map window when town or battle window are open * Fixed switching between pages on small version of spellbook * Saves with long filenames are now truncated in the UI to prevent overflow. @@ -749,10 +884,11 @@ * Fixed incorrect cursor display when hovering over water objects accessible from shore ### Stability + * Fixed a crash when using the 'vcmiobelisk' cheat more than once. * Fixed crash when reaching level 201. The maximum level is now limited to 197. * Fixed crash when accessing a spell with an invalid SPELLCASTER bonus -* Fixed crash when trying to play music for an inaccessible tile +* Fixed crash when trying to play music for an inaccessible tile * Fixed memory corruption on loading of old mods with illegal 'index' field * Fixed possible crash on server shutdown on Android * Fixed possible crash when the affinity of the hero class is set to an invalid value @@ -760,11 +896,13 @@ * Failure to initialise video subsystem now displays error message instead of silent crash ### Random Maps Generator + * Fixed possible creation of a duplicate hero in a random map when the player has chosen the starting hero. * Fixed banning of quest artifacts on random maps * Fixed banning of heroes in prison on random maps ### Battles + * Battle turn queue now displays current turn * Added option to show unit statistics sidebar in battle * Right-clicking on a unit in the battle turn queue now displays the unit details popup. @@ -775,6 +913,7 @@ * Coronius specialty will now correctly select affected units ### Launcher + * Welcome screen will automatically detect existing Heroes 3 installation on Windows * It is now possible to install mods by dragging and dropping onto the launcher. * It is now possible to install maps and campaigns by dragging and dropping onto the launcher. @@ -782,15 +921,18 @@ * Added option to select preferred SDL driver in launcher ### Map Editor + * Fixed saving of allowed abilities, spells, artifacts or heroes ### AI + * AI will no longer attempt to move immobilized units, such as those under the effect of Dendroid Bind. * Fixed shooters not shooting when they have a range penalty * Fixed Fire Elemental spell casting * Fixed rare bug where unit would sometimes do nothing in battle ### Modding + * Added better reporting of "invalid identifiers" errors with suggestions on how to fix them * Added FEROCITY bonus (HotA Aysiud) * Added ENEMY_ATTACK_REDUCTION bonus (HotA Nix) @@ -802,9 +944,10 @@ * BLOCKS_RETALIATION now also blocks FIRST_STRIKE bonus * Added 'canCastOnSelf' field for spells to allow creatures to cast spells on themselves. -# 1.4.1 -> 1.4.2 +## 1.4.1 -> 1.4.2 ### General + * Restored support for Windows 7 * Restored support for 32-bit builds * Implemented quick backpack window for slot-specific artifact selection, activated via mouse wheel / swipe gesture @@ -818,10 +961,12 @@ * added nwctheone / vcmigod cheat: reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight to currently selected hero ### Launcher + * Launcher will now properly show mod installation progress * Launcher will now correctly select preferred language on first start ### Multiplayer + * Timers for all players will now be visible at once * Turn options menu will correctly open for guests when host switches to it * Guests will correctly see which roads are allowed for random maps by host @@ -832,6 +977,7 @@ * Game will now send notifications to players when simultaneous turns end ### Stability + * Fixed crash on clicking town or hero list on MacOS and iOS * Fixed crash on closing vcmi on Android * Fixed crash on disconnection from multiplayer game @@ -848,6 +994,7 @@ * Added check for presence of Armageddon Blade campaign files to avoid crash on some Heroes 3 versions ### Random Maps Generator + * Improved performance of random maps generation * Rebalance of treasure values and density * Improve junction zones generation by spacing Monoliths @@ -860,11 +1007,12 @@ * Fixed spawning of Armageddon's Blade and Vial of Dragon Blood on random maps ### Interface + * Right-clicking hero icon during levelup dialog will now show hero status window * Added indicator of current turn to unit turn order panel in battles * Reduces upscaling artifacts on large spellbook * Game will now display correct date of saved games on Android -* Fixed black screen appearing during spellbook page flip animation +* Fixed black screen appearing during spellbook page flip animation * Fixed description of "Start map with hero" bonus in campaigns * Fixed invisible chat text input in game lobby * Fixed positioning of chat history in game lobby @@ -872,6 +1020,7 @@ * "Large Spellbook" option is now enabled by default ### Mechanics + * Anti-magic garrison now actually blocks spell casting * Berserk spell will no longer cancel if affected unit performs counterattack * Frenzy spell can no longer be casted on units that should be immune to it @@ -879,12 +1028,14 @@ * Vitality and damage skills of a commander will now correctly grow with level ### Modding + * Added UNTIL_OWN_ATTACK duration type for bonuses * Configurable objects with visit mode "first" and "random" now respect "canRefuse" flag -# 1.4.0 -> 1.4.1 +## 1.4.0 -> 1.4.1 ### General + * Fixed position for interaction with starting heroes * Fixed smooth map scrolling when running at high framerate * Fixed calculation of Fire Shield damage when caster has artifacts that increase its damage @@ -901,15 +1052,17 @@ * Reverted ban on U-turns in pathfinder ### Stability + * Fixed crash on using mods made for VCMI 1.3 * Fixed crash on generating random map with large number of monoliths * Fixed crash on losing mission-critical hero in battle * Fixed crash on generating growth detalization in some localizations * Fixed crash on loading of some user-made maps -# 1.3.2 -> 1.4.0 +## 1.3.2 -> 1.4.0 ### General + * Implemented High Score screen * Implemented tracking of completed campaigns * "Secret" Heroes 3 campaigns now require completion of prerequisite campaigns first @@ -933,6 +1086,7 @@ * Spectator mode in single player is now disabled ### Multiplayer + * Implemented simultaneous turns * Implemented turn timers, including chess timers version * Game will now hide entire adventure map on hotseat turn transfer @@ -942,10 +1096,12 @@ * Multiple fixes to validation of player requests by server ### Android + * Heroes 3 data import now accepts files in any case * Fixed detection of Heroes 3 data presence when 'data' directory uses lower case ### Touchscreen + * Added tutorial video clips that explain supported touch gestures * Double tap will now be correctly interpreted as double click, e.g. to start scenario via double-click * Implemented snapping to 100% scale for adventure map zooming @@ -954,6 +1110,7 @@ * Implemented radial wheel for hero exchange in towns ### Launcher + * When a mod is being downloaded, the launcher will now correctly show progress as well as its total size * Double-clicking mod name will now perform expected action, e.g. install/update/enable or disable * Launcher will now show mod extraction progress instead of freezing @@ -965,6 +1122,7 @@ * Added option to reconnect to game lobby ### Editor + * It is now possible to configure rewards for Seer Hut, Pandora Boxes and Events * It is now possible to configure quest (limiter) in Seer Hut and Quest Guards * It is now possible to configure events and rumors in map editor @@ -972,14 +1130,15 @@ * Added option to customize hero skills * It is now possible to select object on map for win/loss conditions or for main town * Random dwellings can now be linked to a random town -* Added map editor zoom -* Added objects lock functionality +* Added map editor zoom +* Added objects lock functionality * It is now possible to configure hero placeholders in map editor -* Fixed duplicate artifact image on mouse drag +* Fixed duplicate artifact image on mouse drag * Lasso tool will no longer skip tiles * Fixed layout of roads and rivers ### Stability + * Fix possible crash on generating random map * Fixed multiple memory leaks in game client * Fixed crash on casting Hypnotize multiple times @@ -988,6 +1147,7 @@ * Fixed crash on clicking on empty Altar of Sacrifice slots ### AI + * BattleAI should now see strong stacks even if blocked by weak stacks. * BattleAI will now prefers targets slower than own stack even if they are not reachable this turn. * Improved BattleAI performance when selecting spell to cast @@ -996,11 +1156,13 @@ * Nullkiller AI can now use Fly and Water Walk spells ### Campaigns + * Implemented voice-over audio support for Heroes 3 campaigns -* Fixes victory condition on 1st scenario of "Long Live the King" campaign +* Fixes victory condition on 1st scenario of "Long Live the King" campaign * Fixed loading of defeat/victory icon and message for some campaign scenarios ### Interface + * Implemented adventure map dimming on opening windows * Clicking town hall icon on town screen will now open town hall * Clicking buildings in town hall will now show which resources are missing (if any) @@ -1015,6 +1177,7 @@ * Attempting to recruit creature in town with no free slots in garrisons will now correctly show error message ### Main Menu + * Implemented window for quick selection of starting hero, town and bonus * Implemented map preview in scenario selection and game load screen accessible via right click on map * Show exact map size in map selection @@ -1030,6 +1193,7 @@ * Main menu animation will no longer appear on top of new game / load game text ### Adventure Map Interface + * Picking up an artifact on adventure map will now show artifact assembly dialog if such option exists * Minimap will now preserve correct aspect ratio on rectangular maps * Fixed slot highlighting when an artifact is being assembled @@ -1047,6 +1211,7 @@ * Right-clicking objects that give bonus to hero will show object description ### Mechanics + * Heroes in tavern will correctly lose effects from spells or visited objects on new day * Fixed multiple bugs in offering of Wisdom and Spell Schools on levelup. Mechanic should now work identically to Heroes 3 * Retreated heroes will no longer restore their entire mana pool on new day @@ -1054,7 +1219,7 @@ * Added support for repeatable quests in Seer Huts * Using "Sacrifice All" on Altar will now correctly place all creatures but one on altar * Fixed probabilities of luck and morale -* Blinded stack no longer can get morale +* Blinded stack no longer can get morale * Creature that attacks while standing in moat will now correctly receive moat damage * Player resources are now limited to 1 000 000 000 to prevent overflow * It is no longer possible to escape from town without fort @@ -1066,6 +1231,7 @@ * Gundula is now Offense specialist and not Sorcery, as in H3 ### Random Maps Generator + * Increased tolerance for placement of Subterranean Gates * Game will now select random object template out of available options instead of picking first one * It is no longer possible to create map with a single team @@ -1076,6 +1242,7 @@ * Fixed bug leading to AI players defeated on day one. ### Modding + * All bonuses now require string as a subtype. See documentation for exact list of possible strings for each bonus. * Changes to existing objects parameters in mods will now be applied to ongoing saves * Fixed handling of engine version compatibility check @@ -1096,9 +1263,10 @@ * Object limiter now allows checking whether hero can learn skill * Object reward may now reveal terrain around visiting hero (e.g. Redwood Observatory) -# 1.3.1 -> 1.3.2 +## 1.3.1 -> 1.3.2 ### GENERAL + * VCMI now uses new application icon * Added initial version of Czech translation * Game will now use tile hero is moving from for movement cost calculations, in line with H3 @@ -1110,6 +1278,7 @@ * Added "vcmiartifacts angelWings" form to "give artifacts" cheat ### STABILITY + * Fixed freeze in Launcher on repository checkout and on mod install * Fixed crash on loading VCMI map with placed Abandoned Mine * Fixed crash on loading VCMI map with neutral towns @@ -1118,9 +1287,11 @@ * Fixed crash on switching fullscreen mode during AI turn ### CAMPAIGNS + * Fixed reorderging of hero primary skills after moving to next scenario in campaigns ### BATTLES + * Conquering a town will now correctly award additional 500 experience points * Quick combat is now enabled by default * Fixed invisible creatures from SUMMON_GUARDIANS and TRANSMUTATION bonuses @@ -1133,6 +1304,7 @@ * Long tap during spell casting will now properly abort the spell ### INTERFACE + * Added "Fill all empty slots with 1 creature" option to radial wheel in garrison windows * Context popup for adventure map monsters will now show creature icon * Game will now show correct victory message for gather troops victory condition @@ -1150,17 +1322,20 @@ * Removed invalid error message on attempting to move non-existing unit in exchange window ### RANDOM MAP GENERATOR + * Fixed bug leading to unreachable resources around mines ### MAP EDITOR + * Fixed crash on maps containing abandoned mines * Fixed crash on maps containing neutral objects * Fixed problem with random map initialized in map editor * Fixed problem with initialization of random dwellings -# 1.3.0 -> 1.3.1 +## 1.3.0 -> 1.3.1 + +### GENERAL -### GENERAL: * Fixed framerate drops on hero movement with active hota mod * Fade-out animations will now be skipped when instant hero movement speed is used * Restarting loaded campaign scenario will now correctly reapply starting bonus @@ -1172,7 +1347,8 @@ * Added option to configure reserved screen area in Launcher on iOS * Fixed border scrolling when game window is maximized -### AI PLAYER: +### AI PLAYER + * BattleAI: Improved performance of AI spell selection * NKAI: Fixed freeze on attempt to exchange army between garrisoned and visiting hero * NKAI: Fixed town threat calculation @@ -1180,13 +1356,15 @@ * VCAI: Added workaround to avoid freeze on attempting to reach unreachable location * VCAI: Fixed spellcasting by Archangels -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Fixed placement of roads inside rock in underground * Fixed placement of shifted creature animations from HotA * Fixed placement of treasures at the boundary of wide connections * Added more potential locations for quest artifacts in zone -### STABILITY: +### STABILITY + * When starting client without H3 data game will now show message instead of silently crashing * When starting invalid map in campaign, game will now show message instead of silently crashing * Blocked loading of saves made with different set of mods to prevent crashes @@ -1206,9 +1384,10 @@ * Fixed possible crash on displaying animated main menu * Fixed crash on recruiting hero in town located on the border of map -# 1.2.1 -> 1.3.0 +## 1.2.1 -> 1.3.0 + +### GENERAL -### GENERAL: * Implemented automatic interface scaling to any resolution supported by monitor * Implemented UI scaling option to scale game interface * Game resolution and UI scaling can now be changed without game restart @@ -1226,7 +1405,8 @@ * Fixed artifact lock icon in localized versions of the game * Fixed possible crash on changing hardware cursor -### TOUCHSCREEN SUPPORT: +### TOUCHSCREEN SUPPORT + * VCMI will now properly recognizes touch screen input * Implemented long tap gesture that shows popup window. Tap once more to close popup * Long tap gesture duration can now be configured in settings @@ -1237,7 +1417,8 @@ * Implemented pinch gesture for zooming adventure map * Implemented haptic feedback (vibration) for long press gesture -### LAUNCHER: +### LAUNCHER + * Launcher will now attempt to automatically detect language of OS on first launch * Added "About" tab with information about project and environment * Added separate options for Allied AI and Enemy AI for adventure map @@ -1245,14 +1426,16 @@ * Fixed potential crash on opening mod information for mods with a changelog * Added option to configure number of autosaves -### MAP EDITOR: +### MAP EDITOR + * Fixed crash on cutting random town * Added option to export entire map as an image * Added validation for placing multiple heroes into starting town * It is now possible to have single player on a map * It is now possible to configure teams in editor -### AI PLAYER: +### AI PLAYER + * Fixed potential crash on accessing market (VCAI) * Fixed potentially infinite turns (VCAI) * Reworked object prioritizing @@ -1261,6 +1444,7 @@ * Various behavior fixes ### GAME MECHANICS + * Hero retreating after end of 7th turn will now correctly appear in tavern * Implemented hero backpack limit (disabled by default) * Fixed Admiral's Hat movement points calculation @@ -1276,7 +1460,8 @@ * Rescued hero from prison will now correctly reveal map around him * Lighthouses will no longer give movement bonus on land -### CAMPAIGNS: +### CAMPAIGNS + * Fixed transfer of artifacts into next scenario * Fixed crash on advancing to next scenario with heroes from mods * Fixed handling of "Start with building" campaign bonus @@ -1286,7 +1471,8 @@ * Fixed frequent crash on moving to next scenario during campaign * Fixed inability to dismiss heroes on maps with "capture town" victory condition -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Improved zone placement, shape and connections * Improved zone passability for better gameplay * Improved treasure distribution and treasure values to match SoD closely @@ -1301,7 +1487,8 @@ * RMG will now run faster, utilizing many CPU cores * Removed random seed number from random map description -### INTERFACE: +### INTERFACE + * Adventure map is now scalable and can be used with any resolution without mods * Adventure map interface is now correctly blocked during enemy turn * Visiting creature banks will now show amount of guards in bank @@ -1322,7 +1509,8 @@ * Right-clicking in town fort window will now show creature information popup * Implemented pasting from clipboard (Ctrl+V) for text input -### BATTLES: +### BATTLES + * Implemented Tower moat (Land Mines) * Implemented defence reduction for units in moat * Added option to always show hero status window @@ -1334,7 +1522,8 @@ * Added distinct overlay image for showing movement range of highlighted unit * Added overlay for displaying shooting range penalties of units -### MODDING: +### MODDING + * Implemented initial version of VCMI campaign format * Implemented spell cast as possible reward for configurable object * Implemented support for configurable buildings in towns @@ -1357,9 +1546,10 @@ * Configurable objects can now be translated * Fixed loading of custom battlefield identifiers for map objects -# 1.2.0 -> 1.2.1 +## 1.2.0 -> 1.2.1 + +### GENERAL -### GENERAL: * Implemented spell range overlay for Dimension Door and Scuttle Boat * Fixed movement cost penalty from terrain * Fixed empty Black Market on game start @@ -1380,9 +1570,10 @@ * Map editor will now correctly save message property for events and pandoras * Fixed incorrect saving of heroes portraits in editor -# 1.1.1 -> 1.2.0 +## 1.1.1 -> 1.2.0 + +### GENERAL -### GENERAL: * Adventure map rendering was entirely rewritten with better, more functional code * Client battle code was heavily reworked, leading to better visual look & feel and fixing multiple minor battle bugs / glitches * Client mechanics are now framerate-independent, rather than speeding up with higher framerate @@ -1393,7 +1584,7 @@ * Fixed bonus values of heroes who specialize in secondary skills * Fixed bonus values of heroes who specialize in creatures * Fixed damage increase from Adela's Bless specialty -* Fixed missing obstacles in battles on subterranean terrain +* Fixed missing obstacles in battles on subterranean terrain * Video files now play at correct speed * Fixed crash on switching to second mission in campaigns * New cheat code: vcmiazure - give 5000 azure dragons in every empty slot @@ -1409,14 +1600,16 @@ * Default game difficulty is now set to "normal" instead of "easy" * Fixed crash on missing music files -### MAP EDITOR: +### MAP EDITOR + * Added translations to German, Polish, Russian, Spanish, Ukrainian * Implemented cut/copy/paste operations * Implemented lasso brush for terrain editing * Toolbar actions now have names * Added basic victory and lose conditions -### LAUNCHER: +### LAUNCHER + * Added initial Welcome/Setup screen for new players * Added option to install translation mod if such mod exists and player's H3 version has different language * Icons now have higher resolution, to prevent upscaling artifacts @@ -1429,7 +1622,8 @@ * Launcher now uses separate mod repository from vcmi-1.1 version to prevent mod updates to unsupported versions * Size of mod list and mod details sub-windows can now be adjusted by player -### AI PLAYER: +### AI PLAYER + * Nullkiller AI is now used by default * AI should now be more active in destroying heroes causing treat on AI towns * AI now has higher priority for resource-producing mines @@ -1445,6 +1639,7 @@ * AI will consider retreat during siege if it can not do anything (catapult is destroyed, no destroyed walls exist) ### RANDOM MAP GENERATOR + * Random map generator can now be used without vcmi-extras mod * RMG will no longer place shipyards or boats at very small lakes * Fixed placement of shipyards in invalid locations @@ -1462,7 +1657,8 @@ * Fixed amount of creatures found in Pandora Boxes to match H3 * Visitable objects will no longer be placed on top of the map, obscured by map border -### ADVENTURE MAP: +### ADVENTURE MAP + * Added option to replace popup messages on object visiting with messages in status window * Implemented different hero movement sounds for offroad movement * Cartographers now reveal terrain in the same way as in H3 @@ -1487,6 +1683,7 @@ * Seer Hut tooltips will now show messages for correct quest type ### INTERFACE + * Implemented new settings window * Added framerate display option * Fixed white status bar on server connection screen @@ -1499,12 +1696,14 @@ * Implemented extended options for random map tab: generate G+U size, select RMG template, manage teams and roads ### HERO SCREEN + * Fixed cases of incorrect artifact slot highlighting * Improved performance of artifact exchange operation * Picking up composite artifact will immediately unlock slots * It is now possible to swap two composite artifacts ### TOWN SCREEN + * Fixed gradual fade-in of a newly built building * Fixed duration of building fade-in to match H3 * Fixed rendering of Shipyard in Castle @@ -1514,7 +1713,8 @@ * Fixed missing left-click message popup for some town buildings * Moving hero from garrison by pressing space will now correctly show message "Cannot have more than 8 adventuring heroes" -### BATTLES: +### BATTLES + * Added settings for even faster animation speed than in H3 * Added display of potential kills numbers into attack tooltip in status bar * Added option to skip battle opening music entirely @@ -1533,7 +1733,7 @@ * Arrow Tower base damage should now match H3 * Destruction of wall segments will now remove ranged attack penalty * Force Field cast in front of drawbridge will now block it as in H3 -* Fixed computations for Behemoth defense reduction ability +* Fixed computations for Behemoth defense reduction ability * Bad luck (if enabled) will now multiple all damage by 50%, in line with other damage reducing mechanics * Fixed highlighting of movement range for creatures standing on a corpse * All battle animations now have same duration/speed as in H3 @@ -1541,7 +1741,7 @@ * Fixed visibility of blue border around targeted creature when spellcaster is making turn * Fixed selection highlight when in targeted creature spellcasting mode * Hovering over hero now correctly shows hero cursor -* Creature currently making turn is now highlighted in the Battle Queue +* Creature currently making turn is now highlighted in the Battle Queue * Hovering over creature icon in Battle Queue will highlight this creature in the battlefield * New battle UI extension allows control over creatures' special abilities * Fixed crash on activating auto-combat in battle @@ -1550,7 +1750,8 @@ * Unicorn Magic Damper Aura ability now works multiplicatively with Resistance * Orb of Vulnerability will now negate Resistance skill -### SPELLS: +### SPELLS + * Hero casting animation will play before spell effect * Fire Shield: added sound effect * Fire Shield: effect now correctly plays on defending creature @@ -1571,7 +1772,8 @@ * All spells that can affecte multiple targets will now highlight affected stacks * Bless and Curse now provide +1 or -1 to base damage on Advanced & Expert levels -### ABILITIES: +### ABILITIES + * Rebirth (Phoenix): Sound will now play in the same time as animation effect * Master Genie spellcasting: Sound will now play in the same time as animation effect * Power Lich, Magogs: Sound will now play in the same time as attack animation effect @@ -1581,13 +1783,14 @@ * Blind: Stacks will no longer retaliate on attack that blinds them * Demon Summon: Added animation effect for summoning * Fire shield will no longer trigger on non-adjacent attacks, e.g. from Dragon Breath -* Weakness now has correct visual effect +* Weakness now has correct visual effect * Added damage bonus for opposite elements for Elementals * Added damage reduction for Magic Elemental attacks against creatures immune to magic * Added incoming damage reduction to Petrify * Added counter-attack damage reduction for Paralyze -### MODDING: +### MODDING + * All configurable objects from H3 now have their configuration in json * Improvements to functionality of configurable objects * Replaced `SECONDARY_SKILL_PREMY` bonus with separate bonuses for each skill. @@ -1608,10 +1811,11 @@ * It is now possible for spellcaster units to have multiple spells (but only for targeting different units) * Fixed incorrect resolving of identifiers in commander abilities and stack experience definitions -# 1.1.0 -> 1.1.1 +## 1.1.0 -> 1.1.1 -### GENERAL: -* Fixed missing sound in Polish version from gog.com +### GENERAL + +* Fixed missing sound in Polish version from gog.com * Fixed positioning of main menu buttons in localized versions of H3 * Fixed crash on transferring artifact to commander * Fixed game freeze on receiving multiple artifact assembly dialogs after combat @@ -1622,27 +1826,32 @@ * Improved map loading speed * Ubuntu PPA: game will no longer crash on assertion failure -### ADVENTURE MAP: +### ADVENTURE MAP + * Fixed hero movement lag in single-player games * Fixed number of drowned troops on visiting Sirens to match H3 * iOS: pinch gesture visits current object (Spacebar behavior) instead of activating in-game console -### TOWNS: +### TOWNS + * Fixed displaying growth bonus from Statue of Legion * Growth bonus tooltip ordering now matches H3 * Buy All Units dialog will now buy units starting from the highest level -### LAUNCHER: +### LAUNCHER + * Local mods can be disabled or uninstalled * Fixed styling of Launcher interface -### MAP EDITOR: +### MAP EDITOR + * Fixed saving of roads and rivers * Fixed placement of heroes on map -# 1.0.0 -> 1.1.0 +## 1.0.0 -> 1.1.0 + +### GENERAL -### 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 @@ -1650,7 +1859,8 @@ * FFMpeg dependency is optional now * Conan package manager is supported for MacOS and iOS -### MULTIPLAYER: +### 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 @@ -1659,7 +1869,8 @@ * Reconnection mode for crashed client processes * Playing online is available using proxy server -### ADVENTURE MAP: +### ADVENTURE MAP + * Fix for digging while opponent's turn * Supported right click for quick recruit window * Fixed problem with quests are requiring identical artefacts @@ -1668,27 +1879,31 @@ * 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 visibility of newly recruited heroes near town * Fixed missing artifact slot in Artifact Merchant window -### BATTLES: +### 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: +### 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: +### RANDOM MAP GENERATOR + * Speedup generation of random maps * Necromancy cannot be learned in Witch Hut on random maps -### MODS: +### MODS + * Supported rewardable objects customization * Battleground obstacles are extendable now with VLC mechanism * Introduced "compatibility" section into mods settings @@ -1696,19 +1911,22 @@ * Supported customisable town entrance placement * Fixed validation of mods with new adventure map objects -### LAUNCHER: +### 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: +### MAP EDITOR + * Basic version of Qt-based map editor -# 0.99 -> 1.0.0 +## 0.99 -> 1.0.0 + +### GENERAL -### GENERAL: * Spectator mode was implemented through command-line options * Some main menu settings get saved after returning to main menu - last selected map, save etc. * Restart scenario button should work correctly now @@ -1716,30 +1934,33 @@ * Lodestar Grail implemented * Fixed Gargoyles immunity * New bonuses: - * SOUL_STEAL - "WoG ghost" ability, should work somewhat same as in H3 - * TRANSMUTATION - "WoG werewolf"-like ability - * SUMMON_GUARDIANS - "WoG santa gremlin"-like ability + two-hex unit extension - * CATAPULT_EXTRA_SHOTS - defines number of extra wall attacks for units that can do so - * RANGED_RETALIATION - allows ranged counterattack - * BLOCKS_RANGED_RETALIATION - disallow enemy ranged counterattack - * SECONDARY_SKILL_VAL2 - set additional parameter for certain secondary skills - * MANUAL_CONTROL - grant manual control over war machine - * WIDE_BREATH - melee creature attacks affect many nearby hexes - * FIRST_STRIKE - creature counterattacks before attack if possible - * SYNERGY_TARGET - placeholder bonus for Mod Design Team (subject to removal in future) - * SHOOTS_ALL_ADJACENT - makes creature shots affect all neighbouring hexes - * BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this - * DESTRUCTION - creature ability for killing extra units after hit, configurable + * SOUL_STEAL - "WoG ghost" ability, should work somewhat same as in H3 + * TRANSMUTATION - "WoG werewolf"-like ability + * SUMMON_GUARDIANS - "WoG santa gremlin"-like ability + two-hex unit extension + * CATAPULT_EXTRA_SHOTS - defines number of extra wall attacks for units that can do so + * RANGED_RETALIATION - allows ranged counterattack + * BLOCKS_RANGED_RETALIATION - disallow enemy ranged counterattack + * SECONDARY_SKILL_VAL2 - set additional parameter for certain secondary skills + * MANUAL_CONTROL - grant manual control over war machine + * WIDE_BREATH - melee creature attacks affect many nearby hexes + * FIRST_STRIKE - creature counterattacks before attack if possible + * SYNERGY_TARGET - placeholder bonus for Mod Design Team (subject to removal in future) + * SHOOTS_ALL_ADJACENT - makes creature shots affect all neighbouring hexes + * BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this + * DESTRUCTION - creature ability for killing extra units after hit, configurable + +### MULTIPLAYER -### MULTIPLAYER: * Loading support. Save from single client could be used to load all clients. * Restart support. All clients will restart together on same server. * Hotseat mixed with network game. Multiple colors can be controlled by each client. -### SPELLS: +### SPELLS + * Implemented cumulative effects for spells -### MODS: +### MODS + * Improve support for WoG commander artifacts and skill descriptions * Added support for modding of original secondary skills and creation of new ones. * Map object sounds can now be configured via json @@ -1748,19 +1969,21 @@ * Added bonus limiters: alignment, faction and terrain * Supported new terrains, new battlefields, custom water and rock terrains * Following special buildings becomes available in the fan towns: - * attackVisitingBonus - * defenceVisitingBonus - * spellPowerVisitingBonus - * knowledgeVisitingBonus - * experienceVisitingBonus - * lighthouse - * treasury + * attackVisitingBonus + * defenceVisitingBonus + * spellPowerVisitingBonus + * knowledgeVisitingBonus + * experienceVisitingBonus + * lighthouse + * treasury + +### SOUND -### SOUND: * Fixed many missing or wrong pickup and visit sounds for map objects * All map objects now have ambient sounds identical to OH3 -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Random map generator supports water modes (normal, islands) * Added config randomMap.json with settings for map generator * Added parameter for template allowedWaterContent @@ -1771,7 +1994,8 @@ * RMG works more stable, various crashes have been fixed * Treasures requiring guards are guaranteed to be protected -### VCAI: +### VCAI + * Reworked goal decomposition engine, fixing many loopholes. AI will now pick correct goals faster. * AI will now use universal pathfinding globally * AI can use Summon Boat and Town Portal @@ -1780,50 +2004,57 @@ * AI can distinguish the value of all map objects * General speed optimizations -### BATTLES: +### BATTLES + * Towers should block ranged retaliation * AI can bypass broken wall with moat instead of standing and waiting until gate is destroyed * Towers do not attack war machines automatically * Draw is possible now as battle outcome in case the battle ends with only summoned creatures (both sides loose) -### ADVENTURE MAP: +### 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: +### LAUNCHER + * Implemented notifications about updates * Supported redirection links for downloading mods -# 0.98 -> 0.99 +## 0.98 -> 0.99 + +### GENERAL -### GENERAL: * New Bonus NO_TERRAIN_PENALTY * Nomads will remove Sand movement penalty from army * Flying and water walking is now supported in pathfinder * New artifacts supported - * Angel Wings - * Boots of Levitation + * Angel Wings + * Boots of Levitation * Implemented rumors in tavern window * New cheat codes: - * vcmiglaurung - gives 5000 crystal dragons into each slot - * vcmiungoliant - conceal fog of war for current player + * vcmiglaurung - gives 5000 crystal dragons into each slot + * vcmiungoliant - conceal fog of war for current player * New console commands: - * gosolo - AI take control over human players and vice versa - * controlai - give control of one or all AIs to player - * set hideSystemMessages on/off - suppress server messages in chat + * gosolo - AI take control over human players and vice versa + * controlai - give control of one or all AIs to player + * set hideSystemMessages on/off - suppress server messages in chat + +### BATTLES -### BATTLES: * Drawbridge mechanics implemented (animation still missing) * Merging of town and visiting hero armies on siege implemented * Hero info tooltip for skills and mana implemented -### ADVENTURE AI: +### ADVENTURE AI + * Fixed AI trying to go through underground rock * Fixed several cases causing AI wandering aimlessly * AI can again pick best artifacts and exchange artifacts between heroes * AI heroes with patrol enabled won't leave patrol area anymore -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Changed fractalization algorithm so it can create cycles * Zones will not have straight paths anymore, they are totally random * Generated zones will have different size depending on template setting @@ -1831,94 +2062,112 @@ * Added Seer Huts with quests that match OH3 * RMG will guarantee at least 100 pairs of Monoliths are available even if there are not enough different defs -# 0.97 -> 0.98 +## 0.97 -> 0.98 + +### GENERAL -### GENERAL: * Pathfinder can now find way using Monoliths and Whirlpools (only used if hero has protection) -### ADVENTURE AI: +### ADVENTURE AI + * AI will try to use Monolith entrances for exploration * AI will now always revisit each exit of two way monolith if exit no longer visible * AI will eagerly pick guarded and blocked treasures -### ADVENTURE MAP: +### ADVENTURE MAP + * Implemented world view * Added graphical fading effects -### SPELLS: +### SPELLS + * New spells handled: - * Earthquake - * View Air - * View Earth - * Visions - * Disguise + * Earthquake + * View Air + * View Earth + * Visions + * Disguise * Implemented CURE spell negative dispel effect * Added LOCATION target for spells castable on any hex with new target modifiers -### BATTLES: +### BATTLES + * Implemented OH3 stack split / upgrade formulas according to AlexSpl -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Underground tunnels are working now * Implemented "junction" zone type * Improved zone placing algorithm * More balanced distribution of treasure piles * More obstacles within zones -# 0.96 -> 0.97 (Nov 01 2014) +## 0.96 -> 0.97 (Nov 01 2014) + +### GENERAL -### GENERAL: * (windows) Moved VCMI data directory from '%userprofile%\vcmi' to '%userprofile%\Documents\My Games\vcmi' * (windows) (OSX) Moved VCMI save directory from 'VCMI_DATA\Games' to 'VCMI_DATA\Saves' * (linux) * Changes in used librries: - * VCMI can now be compiled with SDL2 - * Movies will use ffmpeg library - * change boost::bind to std::bind - * removed boost::assign - * Updated FuzzyLite to 5.0 + * VCMI can now be compiled with SDL2 + * Movies will use ffmpeg library + * change boost::bind to std::bind + * removed boost::assign + * Updated FuzzyLite to 5.0 * Multiplayer load support was implemented through command-line options -### ADVENTURE AI: +### ADVENTURE AI + * Significantly optimized execution time, AI should be much faster now. -### ADVENTURE MAP: +### ADVENTURE MAP + * Non-latin characters can now be entered in chat window or used for save names. * Implemented separate speed for owned heroes and heroes owned by other players -### GRAPHICS: +### GRAPHICS + * Better upscaling when running in fullscreen mode. * New creature/commader window * New resolutions and bonus icons are now part of a separate mod * Added graphics for GENERAL_DAMAGE_REDUCTION bonus (Kuririn) -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Random map generator now creates complete and playable maps, should match original RMG * All important features from original map templates are implemented * Fixed major crash on removing objects * Undeground zones will look just like surface zones -### LAUNCHER: +### LAUNCHER + * Implemented switch to disable intro movies in game -# 0.95 -> 0.96 (Jul 01 2014) +## 0.95 -> 0.96 (Jul 01 2014) -### GENERAL: -* (linux) now VCMI follows XDG specifications. See http://forum.vcmi.eu/viewtopic.php?t=858 +### GENERAL + +* (linux) now VCMI follows XDG specifications. See + +### ADVENTURE AI -### ADVENTURE AI: * Optimized speed and removed various bottlenecks. -### ADVENTURE MAP: +### ADVENTURE MAP + * Heroes auto-level primary and secondary skill levels according to experience -### BATTLES: +### BATTLES + * Wall hit/miss sound will be played when using catapult during siege -### SPELLS: +### SPELLS + * New configuration format -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Towns from mods can be used * Reading connections, terrains, towns and mines from template * Zone placement @@ -1926,31 +2175,35 @@ * Guard generation * Treasure piles generation (so far only few removable objects) -### MODS: +### MODS + * Support for submods - mod may have their own "submods" located in /Mods directory * Mods may provide their own changelogs and screenshots that will be visible in Launcher * Mods can now add new (offensive, buffs, debuffs) spells and change existing * Mods can use custom mage guild background pictures and videos for taverns, setting of resources daily income for buildings -### GENERAL: +### GENERAL + * Added configuring of heroes quantity per player allowed in game -# 0.94 -> 0.95 (Mar 01 2014) +## 0.94 -> 0.95 (Mar 01 2014) + +### GENERAL -### GENERAL: * Components of combined artifacts will now display info about entire set. * Implements level limit * Added WoG creature abilities by Kuririn -* Implemented a confirmation dialog when pressing Alt + F4 to quit the game +* Implemented a confirmation dialog when pressing Alt + F4 to quit the game * Added precompiled header compilation for CMake (can be enabled per flag) * VCMI will detect changes in text files using crc-32 checksum * Basic support for unicode. Internally vcmi always uses utf-8 * (linux) Launcher will be available as "VCMI" menu entry from system menu/launcher * (linux) Added a SIGSEV violation handler to vcmiserver executable for logging stacktrace (for convenience) -### ADVENTURE AI: +### ADVENTURE AI + * AI will use fuzzy logic to compare and choose multiple possible subgoals. -* AI will now use SectorMap to find a way to guarded / covered objects. +* AI will now use SectorMap to find a way to guarded / covered objects. * Significantly improved exploration algorithm. * Locked heroes now try to decompose their goals exhaustively. * Fixed (common) issue when AI found neutral stacks infinitely strong. @@ -1959,26 +2212,30 @@ * AI should now conquer map more aggressively and much faster * Fuzzy rules will be printed out at map launch (if AI log is enabled) -### CAMPAIGNS: +### CAMPAIGNS + * Implemented move heroes to next scenario * Support for non-standard victory conditions for H3 campaigns * Campaigns use window with bonus & scenario selection than scenario information window from normal maps * Implemented hero recreate handling (e.g. Xeron will be recreated on AB campaign) * Moved place bonus hero before normal random hero and starting hero placement -> same behaviour as in OH3 -* Moved placing campaign heroes before random object generation -> same behaviour as in OH3 +* Moved placing campaign heroes before random object generation -> same behaviour as in OH3 + +### TOWNS -### TOWNS: * Extended building dependencies support -### MODS: +### MODS + * Custom victory/loss conditions for maps or campaigns * 7 days without towns loss condition is no longer hardcoded * Only changed mods will be validated -# 0.93 -> 0.94 (Oct 01 2013) +## 0.93 -> 0.94 (Oct 01 2013) -### GENERAL: -* New Launcher application, see +### GENERAL + +* New Launcher application, see * Filesystem now supports zip archives. They can be loaded similarly to other archives in filesystem.json. Mods can use Content.zip instead of Content/ directory. * fixed "get txt" console command * command "extract" to extract file by name @@ -1989,12 +2246,14 @@ * Upgrade cost will never be negative. * support for Chinese fonts (GBK 2-byte encoding) -### ADVENTURE MAP: +### ADVENTURE MAP + * if Quick Combat option is turned on, battles will be resolved by AI * first hero is awakened on new turn * fixed 3000 gems reward in shipwreck -### BATTLES: +### BATTLES + * autofight implemented * most of the animations is time-based * simplified postioning of units in battle, should fix remaining issues with unit positioning @@ -2013,13 +2272,15 @@ * damage done by turrets is properly increased by built buldings * Wyverns will cast Poison instead of Stone Gaze. -### TOWN: +### TOWN + * Fixed issue that allowed to build multiple boats in town. * fix for lookout tower -# 0.92 -> 0.93 (Jun 01 2013) +## 0.92 -> 0.93 (Jun 01 2013) + +### GENERAL -### GENERAL: * Support for SoD-only installations, WoG becomes optional addition * New logging framework * Negative luck support, disabled by default @@ -2027,7 +2288,8 @@ * Fixed stack artifact (and related buttons) not displaying in creature window. * Fixed crash at month of double population. -### MODS: +### MODS + * Improved json validation. Now it support most of features from latest json schema draft. * Icons use path to icon instead of image indexes. * It is possible to edit data of another mod or H3 data via mods. @@ -2035,7 +2297,8 @@ * Removed no longer needed field "projectile spins" * Heroes: split heroes.json in manner similar to creatures\factions; string ID's for H3 heroes; h3 hero classes and artifacts can be modified via json. -### BATTLES: +### BATTLES + * Fixed Death Stare of Commanders * Projectile blitting should be closer to original H3. But still not perfect. * Fixed missing Mirth effects @@ -2044,34 +2307,39 @@ * Fixed abilities of Efreet. * Fixed broken again palette in some battle backgrounds -### TOWN: +### TOWN + * VCMI will not crash if building selection area is smaller than def * Detection of transparency on selection area is closer to H3 * Improved handling buildings with mode "auto": - * they will be properly processed (new creatures will be added if dwelling, spells learned if mage guild, and so on) - * transitive dependencies are handled (A makes B build, and B makes C and D) + * they will be properly processed (new creatures will be added if dwelling, spells learned if mage guild, and so on) + * transitive dependencies are handled (A makes B build, and B makes C and D) + +### SOUND -### SOUND: * Added missing WoG creature sounds (from Kuririn). * The Windows package comes with DLLs needed to play .ogg files * (linux) convertMP3 option for vcmibuilder for systems where SDL_Mixer can't play mp3's * some missing sounds for battle effects -### ARTIFACTS: +### ARTIFACTS + * Several fixes to combined artifacts added via mods. * Fixed Spellbinder's Hat giving level 1 spells instead of 5. * Fixed incorrect components of Cornucopia. * Cheat code with grant all artifacts, including the ones added by mods -# 0.91 -> 0.92 (Mar 01 2013) +## 0.91 -> 0.92 (Mar 01 2013) + +### GENERAL -### GENERAL: * hero crossover between missions in campaigns * introduction before missions in campaigns -### MODS: +### MODS + * Added CREATURE_SPELL_POWER for commanders -* Added spell modifiers to various spells: Hypnotize (Astral), Firewall (Luna), Landmine +* Added spell modifiers to various spells: Hypnotize (Astral), Firewall (Luna), Landmine * Fixed ENEMY_DEFENCE_REDUCTION, GENERAL_ATTACK_REDUCTION * Extended usefulness of ONLY_DISTANCE_FIGHT, ONLY_MELEE_FIGHT ranges * Double growth creatures are configurable now @@ -2079,55 +2347,61 @@ * Stack can use more than 2 attacks. Additional attacks can now be separated as "ONLY_MELEE_FIGHT and "ONLY_DISTANCE_FIGHT". * Moat damage configurable * More config options for spells: - * mind immunity handled by config - * direct damage immunity handled by config - * immunity icon configurable - * removed mind_spell flag -* creature config use string ids now. + * mind immunity handled by config + * direct damage immunity handled by config + * immunity icon configurable + * removed mind_spell flag +* creature config use string ids now. * support for string subtype id in short bonus format * primary skill identifiers for bonuses -# 0.9 -> 0.91 (Feb 01 2013) +## 0.9 -> 0.91 (Feb 01 2013) + +### GENERAL -### GENERAL: * VCMI build on OS X is now supported * Completely removed autotools * Added RMG interace and ability to generate simplest working maps * Added loading screen -### MODS: +### MODS + * Simplified mod structure. Mods from 0.9 will not be compatible. * Mods can be turned on and off in config/modSettings.json file * Support for new factions, including: - * New towns - * New hero classes - * New heroes - * New town-related external dwellings + * New towns + * New hero classes + * New heroes + * New town-related external dwellings * Support for new artifact, including combined, commander and stack artifacts * Extended configuration options - * All game objects are referenced by string identifiers - * Subtype resolution for bonuses + * All game objects are referenced by string identifiers + * Subtype resolution for bonuses + +### BATTLES -### BATTLES: * Support for "enchanted" WoG ability -### ADVENTURE AI: +### ADVENTURE AI + * AI will try to use Subterranean Gate, Redwood Observatory and Cartographer for exploration * Improved exploration algorithm * AI will prioritize dwellings and mines when there are no opponents visible -# 0.89 -> 0.9 (Oct 01 2012) +## 0.89 -> 0.9 (Oct 01 2012) + +### GENERAL -### GENERAL: * Provisional support creature-adding mods * New filesystem allowing easier resource adding/replacing * Reorganized package for better compatibility with HotA and not affecting the original game * Moved many hard-coded settings into text config files * Commander level-up dialog * New Quest Log window -* Fixed a number of bugs in campaigns, support for starting hero selection bonus. +* Fixed a number of bugs in campaigns, support for starting hero selection bonus. + +### BATTLES -### BATTLES: * New graphics for Stack Queue * Death Stare works identically to H3 * No explosion when catapult fails to damage the wall @@ -2135,9 +2409,10 @@ * Fixed crash when attacking stack dies in the Moat just before the attack * Fixed Orb of Inhibition and Recanter's Cloak (they were incorrectly implemented) * Fleeing hero won't lose artifacts. -* Spellbook won't be captured. +* Spellbook won't be captured. + +### ADVENTURE AI -### ADVENTURE AI: * support for quests (Seer Huts, Quest Guardians, and so) * AI will now wander with all the heroes that have spare movement points. It should prevent stalling. * AI will now understand threat of Abandoned Mine. @@ -2147,13 +2422,15 @@ * Fixed crash when hero assigned to goal was lost when attempting realizing it * Fixed a possible freeze when exchanging resources at marketplace -### BATTLE AI: +### BATTLE AI + * It is possible to select a battle AI module used by VCMI by typing into the console "setBattleAI ". The names of available modules are "StupidAI" and "BattleAI". BattleAI may be a little smarter but less stable. By the default, StupidAI will be used, as in previous releases. * New battle AI module: "BattleAI" that is smarter and capable of casting some offensive and enchantment spells -# 0.88 -> 0.89 (Jun 01 2012) +## 0.88 -> 0.89 (Jun 01 2012) + +### GENERAL -### GENERAL: * Mostly implemented Commanders feature (missing level-up dialog) * Support for stack artifacts * New creature window graphics contributed by fishkebab @@ -2165,11 +2442,13 @@ * Simple mechanism for detecting game desynchronization after init * 1280x800 resolution graphics, contributed by Topas -### ADVENTURE MAP: +### ADVENTURE MAP + * Fixed monsters regenerating casualties from battle at the start of new week. * T in adventure map will switch to next town -### BATTLES: +### BATTLES + * It's possible to switch active creature during tacts phase by clicking on stack * After battle artifacts of the defeated hero (and his army) will be taken by winner * Rewritten handling of battle obstacles. They will be now placed following H3 algorithm. @@ -2186,22 +2465,25 @@ * Fixed and simplified Teleport casting * Fixed Remove Obstacle spell * New spells supported: - * Chain Lightning - * Fire Wall - * Force Field - * Land Mine - * Quicksands - * Sacrifice + * Chain Lightning + * Fire Wall + * Force Field + * Land Mine + * Quicksands + * Sacrifice + +### TOWNS -### TOWNS: * T in castle window will open a tavern window (if available) -### PREGAME: +### PREGAME + * Pregame will use same resolution as main game * Support for scaling background image * Customization of graphics with config file. -### ADVENTURE AI: +### ADVENTURE AI + * basic rule system for threat evaluation * new town development logic * AI can now use external dwellings @@ -2210,7 +2492,7 @@ * AI will recruit multiple heroes for exploration * AI won't try attacking its own heroes -# 0.87 -> 0.88 (Mar 01 2012) +## 0.87 -> 0.88 (Mar 01 2012) * added an initial version of new adventure AI: VCAI * system settings window allows to change default resolution @@ -2219,202 +2501,226 @@ * Creature Window can handle descriptions of spellcasting abilities * Support for the clone spell -# 0.86 -> 0.87 (Dec 01 2011) +## 0.86 -> 0.87 (Dec 01 2011) + +### GENERAL -### GENERAL: * Pathfinder can find way using ships and subterranean gates * Hero reminder & sleep button -### PREGAME: +### PREGAME + * Credits are implemented -### BATTLES: +### BATTLES + * All attacked hexes will be highlighted * New combat abilities supported: - * Spell Resistance aura - * Random spellcaster (Genies) - * Mana channeling - * Daemon summoning - * Spellcaster (Archangel Ogre Mage, Elementals, Faerie Dragon) - * Fear - * Fearless - * No wall penalty - * Enchanter - * Bind - * Dispel helpful spells + * Spell Resistance aura + * Random spellcaster (Genies) + * Mana channeling + * Daemon summoning + * Spellcaster (Archangel Ogre Mage, Elementals, Faerie Dragon) + * Fear + * Fearless + * No wall penalty + * Enchanter + * Bind + * Dispel helpful spells -# 0.85 -> 0.86 (Sep 01 2011) +## 0.85 -> 0.86 (Sep 01 2011) + +### GENERAL -### GENERAL: * Reinstated music support * Bonus system optimizations (caching) * converted many config files to JSON * .tga file support * New artifacts supported - * Admiral's Hat - * Statue of Legion - * Titan's Thunder + * Admiral's Hat + * Statue of Legion + * Titan's Thunder + +### BATTLES -### BATTLES: * Correct handling of siege obstacles * Catapult animation * New combat abilities supported - * Dragon Breath - * Three-headed Attack - * Attack all around - * Death Cloud / Fireball area attack - * Death Blow - * Lightning Strike - * Rebirth + * Dragon Breath + * Three-headed Attack + * Attack all around + * Death Cloud / Fireball area attack + * Death Blow + * Lightning Strike + * Rebirth * New WoG abilities supported - * Defense Bonus - * Cast before attack - * Immunity to direct damage spells + * Defense Bonus + * Cast before attack + * Immunity to direct damage spells * New spells supported - * Magic Mirror - * Titan's Lightning Bolt + * Magic Mirror + * Titan's Lightning Bolt -# 0.84 -> 0.85 (Jun 01 2011) +## 0.84 -> 0.85 (Jun 01 2011) + +### GENERAL -### GENERAL: * Support for stack experience * Implemented original campaign selection screens * New artifacts supported: - * Statesman's Medal - * Diplomat's Ring - * Ambassador's Sash + * Statesman's Medal + * Diplomat's Ring + * Ambassador's Sash + +### TOWNS -### TOWNS: * Implemented animation for new town buildings * It's possible to sell artifacts at Artifact Merchants -### BATTLES: +### BATTLES + * Neutral monsters will be split into multiple stacks * Hero can surrender battle to keep army * Support for Death Stare, Support for Poison, Age, Disease, Acid Breath, Fire / Water / Earth / Air immunities and Receptiveness * Partial support for Stone Gaze, Paralyze, Mana drain -# 0.83 -> 0.84 (Mar 01 2011) +## 0.83 -> 0.84 (Mar 01 2011) + +### GENERAL -### GENERAL: * Bonus system has been rewritten * Partial support for running VCMI in duel mode (no adventure map, only one battle, ATM only AI-AI battles) * New artifacts supported: - * Angellic Alliance - * Bird of Perception - * Emblem of Cognizance - * Spell Scroll - * Stoic Watchman + * Angellic Alliance + * Bird of Perception + * Emblem of Cognizance + * Spell Scroll + * Stoic Watchman + +### BATTLES -### BATTLES: * Better animations handling * Defensive stance is supported -### HERO: -* New secondary skills supported: - * Artillery - * Eagle Eye - * Tactics +### HERO + +* New secondary skills supported: + * Artillery + * Eagle Eye + * Tactics + +### AI PLAYER -### AI PLAYER: * new AI leading neutral creatures in combat, slightly better then previous -# 0.82 -> 0.83 (Nov 01 2010) +## 0.82 -> 0.83 (Nov 01 2010) + +### GENERAL -### GENERAL: * Alliances support * Week of / Month of events * Mostly done pregame for MP games (temporarily only for local clients) * Support for 16bpp displays * Campaigns: - * support for building bonus - * moving to next map after victory + * support for building bonus + * moving to next map after victory * Town Portal supported * Vial of Dragon Blood and Statue of Legion supported -### HERO: +### HERO + * remaining specialities have been implemented -### TOWNS: -* town events supported -* Support for new town structures: Deiety of Fire and Escape Tunnel +### TOWNS + +* town events supported +* Support for new town structures: Deiety of Fire and Escape Tunnel + +### BATTLES -### BATTLES: * blocked retreating from castle -# 0.81 -> 0.82 (Aug 01 2010) +## 0.81 -> 0.82 (Aug 01 2010) + +### GENERAL -### GENERAL: * Some of the starting bonuses in campaigns are supported * It's possible to select difficulty level of mission in campaign * new cheat codes: - * vcmisilmaril - player wins - * vcmimelkor - player loses + * vcmisilmaril - player wins + * vcmimelkor - player loses + +### ADVENTURE MAP -### ADVENTURE MAP: * Neutral armies growth implemented (10% weekly) * Power rating of neutral stacks * Favourable Winds reduce sailing cost -### HERO: +### HERO + * Learning secondary skill supported. * Most of hero specialities are supported, including: - * Creature specialities (progressive, fixed, Sir Mullich) - * Spell damage specialities (Deemer), fixed bonus (Ciele) - * Secondary skill bonuses - * Creature Upgrades (Gelu) - * Resource generation - * Starting Skill (Adrienne) + * Creature specialities (progressive, fixed, Sir Mullich) + * Spell damage specialities (Deemer), fixed bonus (Ciele) + * Secondary skill bonuses + * Creature Upgrades (Gelu) + * Resource generation + * Starting Skill (Adrienne) + +### TOWNS -### TOWNS: * Support for new town structures: - * Artifact Merchant - * Aurora Borealis - * Castle Gates - * Magic University - * Portal of Summoning - * Skeleton transformer - * Veil of Darkness + * Artifact Merchant + * Aurora Borealis + * Castle Gates + * Magic University + * Portal of Summoning + * Skeleton transformer + * Veil of Darkness + +### OBJECTS -### OBJECTS: * Stables will now upgrade Cavaliers to Champions. * New object supported: - * Abandoned Mine - * Altar of Sacrifice - * Black Market - * Cover of Darkness - * Hill Fort - * Refugee Camp - * Sanctuary - * Tavern - * University - * Whirlpool + * Abandoned Mine + * Altar of Sacrifice + * Black Market + * Cover of Darkness + * Hill Fort + * Refugee Camp + * Sanctuary + * Tavern + * University + * Whirlpool -# 0.8 -> 0.81 (Jun 01 2010) +## 0.8 -> 0.81 (Jun 01 2010) + +### GENERAL -### GENERAL: * It's possible to start campaign * Support for build grail victory condition * New artifacts supported: - * Angel's Wings - * Boots of levitation - * Orb of Vulnerability - * Ammo cart - * Golden Bow - * Hourglass of Evil Hour - * Bow of Sharpshooter - * Armor of the Damned + * Angel's Wings + * Boots of levitation + * Orb of Vulnerability + * Ammo cart + * Golden Bow + * Hourglass of Evil Hour + * Bow of Sharpshooter + * Armor of the Damned + +### ADVENTURE MAP -### ADVENTURE MAP: * Creatures now guard surrounding tiles * New adventura map spells supported: - * Summon Boat - * Scuttle Boat - * Dimension Door - * Fly - * Water walk + * Summon Boat + * Scuttle Boat + * Dimension Door + * Fly + * Water walk + +### BATTLES -### BATTLES: * A number of new creature abilities supported * First Aid Tent is functional * Support for distance/wall/melee penalties & no * penalty abilities @@ -2422,107 +2728,123 @@ * Luck support * Teleportation spell -### HERO: +### HERO + * First Aid secondary skill * Improved formula for necromancy to match better OH3 -### TOWNS: +### TOWNS + * Sending resources to other players by marketplace * Support for new town structures: - * Lighthouse - * Colossus - * Freelancer's Guild - * Guardian Spirit - * Necromancy Amplifier - * Soul Prison + * Lighthouse + * Colossus + * Freelancer's Guild + * Guardian Spirit + * Necromancy Amplifier + * Soul Prison + +### OBJECTS -### OBJECTS: * New object supported: - * Freelancer's Guild - * Trading Post - * War Machine Factory + * Freelancer's Guild + * Trading Post + * War Machine Factory -# 0.75 -> 0.8 (Mar 01 2010) +## 0.75 -> 0.8 (Mar 01 2010) + +### GENERAL -### GENERAL: * Victory and loss conditions are supported. It's now possible to win or lose the game. * Implemented assembling and disassembling of combination artifacts. * Kingdom Overview screen is now available. * Implemented Grail (puzzle map, digging, constructing ultimate building) * Replaced TTF fonts with original ones. -### ADVENTURE MAP: +### ADVENTURE MAP + * Implemented rivers animations (thx to GrayFace). -### BATTLES: +### BATTLES + * Fire Shield spell (and creature ability) supported * affecting morale/luck and casting spell after attack creature abilities supported -### HERO: +### HERO + * Implementation of Scholar secondary skill -### TOWN: +### TOWN + * New left-bottom info panel functionalities. -### TOWNS: -* new town structures supported: - * Ballista Yard - * Blood Obelisk - * Brimstone Clouds - * Dwarven Treasury - * Fountain of Fortune - * Glyphs of Fear - * Mystic Pond - * Thieves Guild - * Special Grail functionalities for Dungeon, Stronghold and Fortress +### TOWNS + +* new town structures supported: + * Ballista Yard + * Blood Obelisk + * Brimstone Clouds + * Dwarven Treasury + * Fountain of Fortune + * Glyphs of Fear + * Mystic Pond + * Thieves Guild + * Special Grail functionalities for Dungeon, Stronghold and Fortress + +### OBJECTS -### OBJECTS: * New objects supported: - * Border gate - * Den of Thieves - * Lighthouse - * Obelisk - * Quest Guard - * Seer hut + * Border gate + * Den of Thieves + * Lighthouse + * Obelisk + * Quest Guard + * Seer hut A lot of of various bugfixes and improvements: -http://bugs.vcmi.eu/changelog_page.php?version_id=14 + -# 0.74 -> 0.75 (Dec 01 2009) +## 0.74 -> 0.75 (Dec 01 2009) + +### GENERAL -### GENERAL: * Implemented "main menu" in-game option. * Hide the mouse cursor while displaying a popup window. * Better handling of huge and empty message boxes (still needs more changes) * Fixed several crashes when exiting. -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * Movement cursor shown for unguarded enemy towns. * Battle cursor shown for guarded enemy garrisons. * Clicking on the border no longer opens an empty info windows -### HERO WINDOW: -* Improved artifact moving. Available slots are highlighted. Moved artifact is bound to mouse cursor. +### HERO WINDOW + +* Improved artifact moving. Available slots are highlighted. Moved artifact is bound to mouse cursor. + +### TOWNS -### TOWNS: * new special town structures supported: - * Academy of Battle Scholars - * Cage of Warlords - * Mana Vortex - * Stables - * Skyship (revealing entire map only) + * Academy of Battle Scholars + * Cage of Warlords + * Mana Vortex + * Stables + * Skyship (revealing entire map only) + +### OBJECTS -### OBJECTS: * External dwellings increase town growth * Right-click info window for castles and garrisons you do not own shows a rough amount of creatures instead of none * Scholar won't give unavailable spells anymore. A lot of of various bugfixes and improvements: -http://bugs.vcmi.eu/changelog_page.php?version_id=2 + -# 0.73 -> 0.74 (Oct 01 2009) +## 0.73 -> 0.74 (Oct 01 2009) + +### GENERAL -### GENERAL: * Scenario Information window * Save Game window * VCMI window should start centered @@ -2534,13 +2856,15 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * fixed issue when splitting stack to the hero with only one creatures * a few fixes for shipyard window -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * Cursor shows if tile is accessible and how many turns away * moving hero with arrow keys / numpad * fixed Next Hero button behaviour * fixed Surface/Underground switch button in higher resolutions -### BATTLES: +### BATTLES + * partial siege support * new stack queue for higher resolutions (graphics made by Dru, thx!) * 'Q' pressing toggles the stack queue displaying (so it can be enabled/disabled it with single key press) @@ -2550,72 +2874,77 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * fixed crash when clicking on enemy stack without moving mouse just after receiving action * even large stack numbers will fit the boxes * when active stack is killed by spell, game behaves properly -* shooters attacking twice (like Grand Elves) won't attack twice in melee -* ballista can shoot even if there's an enemy creature next to it +* shooters attacking twice (like Grand Elves) won't attack twice in melee +* ballista can shoot even if there's an enemy creature next to it * improved obstacles placement, so they'll better fit hexes (thx to Ivan!) * selecting attack directions works as in H3 * estimating damage that will be dealt while choosing stack to be attacked * modified the positioning of battle effects, they should look about right now. -* after selecting a spell during combat, l-click is locked for any action other than casting. +* after selecting a spell during combat, l-click is locked for any action other than casting. * flying creatures will be blitted over all other creatures, obstacles and wall * obstacles and units should be printed in better order (not tested) * fixed armageddon animation * new spells supported: - * Anti-Magic - * Cure - * Resurrection - * Animate Dead - * Counterstrike - * Berserk - * Hypnotize - * Blind - * Fire Elemental - * Earth Elemental - * Water Elemental - * Air Elemental - * Remove obstacle + * Anti-Magic + * Cure + * Resurrection + * Animate Dead + * Counterstrike + * Berserk + * Hypnotize + * Blind + * Fire Elemental + * Earth Elemental + * Water Elemental + * Air Elemental + * Remove obstacle + +### TOWNS -### TOWNS: * enemy castle can be taken over * only one capitol per player allowed (additional ones will be lost) * garrisoned hero can buy a spellbook * heroes available in tavern should be always different * ship bought in town will be correctly placed * new special town structures supported: - * Lookout Tower - * Temple of Valhalla - * Wall of Knowledge - * Order of Fire + * Lookout Tower + * Temple of Valhalla + * Wall of Knowledge + * Order of Fire + +### HERO WINDOW -### HERO WINDOW: * war machines cannot be unequiped -### PREGAME: +### PREGAME + * sorting: a second click on the column header sorts in descending order. * advanced options tab: r-click popups for selected town, hero and bonus * starting scenario / game by double click -* arrows in options tab are hidden when not available +* arrows in options tab are hidden when not available * subtitles for chosen hero/town/bonus in pregame -### OBJECTS: +### OBJECTS + * fixed pairing Subterranean Gates * New objects supported: - * Borderguard & Keymaster Tent - * Cartographer - * Creature banks - * Eye of the Magi & Hut of the Magi - * Garrison - * Stables - * Pandora Box - * Pyramid + * Borderguard & Keymaster Tent + * Cartographer + * Creature banks + * Eye of the Magi & Hut of the Magi + * Garrison + * Stables + * Pandora Box + * Pyramid -# 0.72 -> 0.73 (Aug 01 2009) +## 0.72 -> 0.73 (Aug 01 2009) + +### GENERAL -### GENERAL: * infowindow popup will be completely on screen * fixed possible crash with in game console * fixed crash when gaining artifact after r-click on hero in tavern -* Estates / hero bonuses won't give resources on first day. +* Estates / hero bonuses won't give resources on first day. * video handling (intro, main menu animation, tavern animation, spellbook animation, battle result window) * hero meeting window allowing exchanging armies and artifacts between heroes on adventure map * 'T' hotkey opens marketplace window @@ -2634,13 +2963,15 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * r-click popups on enemy heroes and towns * hero leveling formula matches the H3 -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * Garrisoning, then removing hero from garrison move him at the end of the heroes list * The size of the frame around the map depends on the screen size. * spellbook shows adventure spells when opened on adventure map * erasing path after picking objects with last movement point -### BATTLES: +### BATTLES + * spell resistance supported (secondary skill, artifacts, creature skill) * corrected damage inflicted by spells and ballista * added some missing projectile infos @@ -2649,49 +2980,54 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * non-living and undead creatures have now always 0 morale * displaying luck effect animation * support for battleground overlays: - * cursed ground - * magic plains - * fiery fields - * rock lands - * magic clouds - * lucid pools - * holy ground - * clover field - * evil fog + * cursed ground + * magic plains + * fiery fields + * rock lands + * magic clouds + * lucid pools + * holy ground + * clover field + * evil fog + +### TOWNS -### TOWNS: * fixes for horde buildings * garrisoned hero can buy a spellbook if he is selected or if there is no visiting hero * capitol bar in town hall is grey (not red) if already one exists * fixed crash on entering hall when town was near map edge -### HERO WINDOW: +### HERO WINDOW + * garrisoned heroes won't be shown on the list * artifacts will be present on morale/luck bonuses list -### PREGAME: +### PREGAME + * saves are sorted primary by map format, secondary by name * fixed displaying date of saved game (uses local time, removed square character) -### OBJECTS: +### OBJECTS + * Fixed primary/secondary skill levels given by a scholar. * fixed problems with 3-tiles monoliths * fixed crash with flaggable building next to map edge * fixed some descriptions for events * New objects supported: - * Buoy - * Creature Generators - * Flotsam - * Mermaid - * Ocean bottle - * Sea Chest - * Shipwreck Survivor - * Shipyard - * Sirens + * Buoy + * Creature Generators + * Flotsam + * Mermaid + * Ocean bottle + * Sea Chest + * Shipwreck Survivor + * Shipyard + * Sirens -# 0.71 -> 0.72 (Jun 1 2009) +## 0.71 -> 0.72 (Jun 1 2009) + +### GENERAL -### GENERAL: * many sound effects and music * autosave (to 5 subsequent files) * artifacts support (most of them) @@ -2707,7 +3043,8 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * redundant quotation marks from artifact descriptions are removed * no income at the first day -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * fixed crasbug occurring on revisiting objects (by pressing space) * always restoring default cursor when movng mouse out of the terrain * fixed map scrolling with ctrl+arrows when some windows are opened @@ -2715,7 +3052,8 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * pathfinder will now look for a path going via printed positions of roads when it's possible * enter can be used to open window with selected hero/town -### BATTLES: +### BATTLES + * many creatures special skills implemented * battle will end when one side has only war machines * fixed some problems with handling obstacles info @@ -2725,41 +3063,45 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * canceling of casting a spell by pressing Escape or R-click (R-click on a creatures does not cancel a spell) * spellbook cannot be opened by L-click on hero in battle when it shouldn't be possible * new spells: - * frost ring - * fireball - * inferno - * meteor shower - * death ripple - * destroy undead - * dispel - * armageddon - * disrupting ray - * protection from air - * protection from fire - * protection from water - * protection from earth - * precision - * slayer + * frost ring + * fireball + * inferno + * meteor shower + * death ripple + * destroy undead + * dispel + * armageddon + * disrupting ray + * protection from air + * protection from fire + * protection from water + * protection from earth + * precision + * slayer + +### TOWNS -### TOWNS: * resting in town with mage guild will replenih all the mana points * fixed Blacksmith * the number of creatures at the beginning of game is their base growth * it's possible to enter Tavern via Brotherhood of Sword -### HERO WINDOW: +### HERO WINDOW + * fixed mana limit info in the hero window * war machines can't be removed * fixed problems with removing artifacts when all visible slots in backpack are full -### PREGAME: +### PREGAME + * clicking on "advanced options" a second time now closes the tab instead of refreshing it. -* Fix position of maps names. +* Fix position of maps names. * Made the slider cursor much more responsive. Speedup the map select screen. * Try to behave when no maps/saves are present. * Page Up / Page Down / Home / End hotkeys for scrolling through scenarios / games list -### OBJECTS: +### OBJECTS + * Neutral creatures can join or escape depending on hero strength (escape formula needs to be improved) * leaving guardians in flagged mines. * support for Scholar object @@ -2773,46 +3115,49 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * support for Event * Corpse (Skeleton) will be accessible from all directions -# 0.7 -> 0.71 (Apr 01 2009) +## 0.7 -> 0.71 (Apr 01 2009) -### GENERAL: -* fixed scrolling behind window problem (now it's possible to scroll with CTRL + arrows) -* morale/luck system and corresponding sec. skills supported -* fixed crash when hero get level and has less than two sec. skills to choose between +### GENERAL + +* fixed scrolling behind window problem (now it's possible to scroll with CTRL + arrows) +* morale/luck system and corresponding sec. skills supported +* fixed crash when hero get level and has less than two sec. skills to choose between * added keybindings for components in selection window (eg. for treasure chest dialog): 1, 2, and so on. Selection dialog can be closed with Enter key * proper handling of custom portraits of heroes * fixed problems with non-hero/town defs not present in def list but present on map (occurring probably only in case of def substitution in map editor) -* fixed crash when there was no hero available to hire for some player +* fixed crash when there was no hero available to hire for some player * fixed problems with 1024x600 screen resolution -* updating blockmap/visitmap of randomized objects +* updating blockmap/visitmap of randomized objects * fixed crashes on loading maps with flag all mines/dwelling victory condition * further fixes for leveling-up (stability and identical offered skills bug) * splitting window allows to rebalance two stack with the same creatures * support for numpad keyboard * support for timed events -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * added "Next hero" button functionality * added missing path arrows -* corrected centering on hero's position +* corrected centering on hero's position * recalculating hero path after reselecting hero * further changes in pathfinder making it more like original one -* orientation of hero can't be change if movement points are exhausted +* orientation of hero can't be change if movement points are exhausted * campfire, borderguard, bordergate, questguard will be accessible from the top * new movement cost calculation algorithm * fixed sight radious calculation * it's possible to stop hero movement -* faster minimap refreshing +* faster minimap refreshing * provisional support for "Save" button in System Options Window * it's possible to revisit object under hero by pressing Space -### BATTLES: +### BATTLES + * partial support for battle obstacles * only one spell can be casted per turn * blocked opening sepllbook if hero doesn't have a one -* spells not known by hero can't be casted +* spells not known by hero can't be casted * spell books won't be placed in War Machine slots after battle -* attack is now possible when hex under cursor is not displayed +* attack is now possible when hex under cursor is not displayed * glowing effect of yellow border around creatures * blue glowing border around hovered creature * made animation on battlefield more smooth @@ -2826,82 +3171,90 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * correct handling of flying creatures in battles * a few tweaks in battle path/available hexes calculation (more of them is needed) * amounts of units taking actions / being an object of actions won't be shown until action ends -* fixed positions of stack queue and battle result window when resolution is != 800x600 -* corrected duration of frenzy spell which was incorrect in certain cases +* fixed positions of stack queue and battle result window when resolution is != 800x600 +* corrected duration of frenzy spell which was incorrect in certain cases * corrected hero spell casting animation -* better support for battle backgrounds -* blocked "save" command during battle +* better support for battle backgrounds +* blocked "save" command during battle * spellbook displays only spells known by Hero * New spells supported: - * Mirth - * Sorrow - * Fortune - * Misfortune + * Mirth + * Sorrow + * Fortune + * Misfortune + +### TOWN INTERFACE -### TOWN INTERFACE: * cannot build more than one capitol * cannot build shipyard if town is not near water -* Rampart's Treasury requires Miner's Guild +* Rampart's Treasury requires Miner's Guild * minor improvements in Recruitment Window * fixed crash occurring when clicking on hero portrait in Tavern Window, minor improvements for Tavern Window * proper updating resdatabar after building structure in town or buying creatures (non 800x600 res) -* fixed blinking resdatabar in town screen when buying (800x600) +* fixed blinking resdatabar in town screen when buying (800x600) * fixed horde buildings displaying in town hall * forbidden buildings will be shown as forbidden, even if there are no res / other conditions are not fulfilled -### PREGAME: +### PREGAME + * added scrolling scenario list with mouse wheel * fixed mouse slow downs -* cannot select heroes for computer player (pregame) +* cannot select heroes for computer player (pregame) * no crash if uses gives wrong resolution ID number * minor fixes -### OBJECTS: -* windmill gives 500 gold only during first week ever (not every month) -* After the first visit to the Witch Hut, right-click/hover tip mentions the skill available. -* New objects supported: - * Prison - * Magic Well - * Faerie Ring - * Swan Pond - * Idol of Fortune - * Fountain of Fortune - * Rally Flag - * Oasis - * Temple - * Watering Hole - * Fountain of Youth - * support for Redwood Observatory - * support for Shrine of Magic Incantation / Gesture / Thought - * support for Sign / Ocean Bottle +### OBJECTS + +* windmill gives 500 gold only during first week ever (not every month) +* After the first visit to the Witch Hut, right-click/hover tip mentions the skill available. +* New objects supported: + * Prison + * Magic Well + * Faerie Ring + * Swan Pond + * Idol of Fortune + * Fountain of Fortune + * Rally Flag + * Oasis + * Temple + * Watering Hole + * Fountain of Youth + * support for Redwood Observatory + * support for Shrine of Magic Incantation / Gesture / Thought + * support for Sign / Ocean Bottle + +### AI PLAYER -### AI PLAYER: * Minor improvements and fixes. -# 0.64 -> 0.7 (Feb 01 2009) +## 0.64 -> 0.7 (Feb 01 2009) + +### GENERAL -### GENERAL: * move some settings to the config/settings.txt file * partial support for new screen resolutions -* it's possible to set game resolution in pregame (type 'resolution' in the console) +* it's possible to set game resolution in pregame (type 'resolution' in the console) * /Data and /Sprites subfolders can be used for adding files not present in .lod archives * fixed crashbug occurring when hero levelled above 15 level * support for non-standard screen resolutions * F4 toggles between full-screen and windowed mode * minor improvements in creature card window -* splitting stacks with the shift+click -* creature card window contains info about modified speed +* splitting stacks with the shift+click +* creature card window contains info about modified speed + +### ADVENTURE INTERFACE -### ADVENTURE INTERFACE: * added water animation * speed of scrolling map and hero movement can be adjusted in the System Options Window * partial handling r-clicks on adventure map -### TOWN INTERFACE: +### TOWN INTERFACE + * the scroll tab won't remain hanged to our mouse position if we move the mouse is away from the scroll bar * fixed cloning creatures bug in garrisons (and related issues) -### BATTLES: +### BATTLES + * support for the Wait command * magic arrow *really* works * war machines support partially added @@ -2910,39 +3263,42 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * positive/negative spells cannot be cast on hostile/our stacks * showing spell effects affecting stack in creature info window * more appropriate coloring of stack amount box when stack is affected by a spell -* battle console displays notifications about wait/defend commands +* battle console displays notifications about wait/defend commands * several reported bugs fixed * new spells supported: - * Haste - * lightning bolt - * ice bolt - * slow - * implosion - * forgetfulness - * shield - * air shield - * bless - * curse - * bloodlust - * weakness - * stone skin - * prayer - * frenzy + * Haste + * lightning bolt + * ice bolt + * slow + * implosion + * forgetfulness + * shield + * air shield + * bless + * curse + * bloodlust + * weakness + * stone skin + * prayer + * frenzy + +### AI PLAYER -### AI PLAYER: * Genius AI (first VCMI AI) will control computer creatures during the combat. -### OBJECTS: +### OBJECTS + * Guardians property for resources is handled * support for Witch Hut * support for Arena -* support for Library of Enlightenment +* support for Library of Enlightenment And a lot of minor fixes -# 0.63 -> 0.64 (Nov 01 2008) +## 0.63 -> 0.64 (Nov 01 2008) + +### GENERAL -### GENERAL: * sprites from /Sprites folder are handled correctly * several fixes for pathfinder and path arrows * better handling disposed/predefined heroes @@ -2954,39 +3310,43 @@ And a lot of minor fixes * many minor improvements * Added some kind of simple chatting functionality through console. Implemented several WoG cheats equivalents: - * woggaladriel -> vcmiainur - * wogoliphaunt -> vcminoldor - * wogshadowfax -> vcminahar - * wogeyeofsauron -> vcmieagles - * wogisengard -> vcmiformenos - * wogsaruman -> vcmiistari - * wogpathofthedead -> vcmiangband - * woggandalfwhite -> vcmiglorfindel + * woggaladriel -> vcmiainur + * wogoliphaunt -> vcminoldor + * wogshadowfax -> vcminahar + * wogeyeofsauron -> vcmieagles + * wogisengard -> vcmiformenos + * wogsaruman -> vcmiistari + * wogpathofthedead -> vcmiangband + * woggandalfwhite -> vcmiglorfindel -### ADVENTURE INTERFACE: -* clicking on a tile in advmap view when a path is shown will not only hide it but also calculate a new one -* slowed map scrolling +### ADVENTURE INTERFACE + +* clicking on a tile in advmap view when a path is shown will not only hide it but also calculate a new one +* slowed map scrolling * blocked scrolling adventure map with mouse when left ctrl is pressed * blocked map scrolling when dialog window is opened * scholar will be accessible from the top -### TOWN INTERFACE: +### TOWN INTERFACE + * partially done tavern window (only hero hiring functionality) -### BATTLES: +### BATTLES + * water elemental will really be treated as 2 hex creature * potential infinite loop in reverseCreature removed -* better handling of battle cursor +* better handling of battle cursor * fixed blocked shooter behavior * it's possible in battles to check remeaining HP of neutral stacks * partial support for Magic Arrow spell * fixed bug with dying unit * stack queue hotkey is now 'Q' -* added shots limit +* added shots limit -# 0.62 -> 0.63 (Oct 01 2008) +## 0.62 -> 0.63 (Oct 01 2008) + +### GENERAL -### GENERAL: * coloured console output, logging all info to txt files * it's possible to use other port than 3030 by passing it as an additional argument * removed some redundant warnings @@ -2995,49 +3355,54 @@ And a lot of minor fixes * some crashbugs was fixed * added handling of navigation, logistics, pathfinding, scouting end estates secondary skill * magical hero are given spellbook at the beginning -* added initial secondary skills for heroes +* added initial secondary skills for heroes -### BATTLES: -* very significant optimization of battles +### BATTLES + +* very significant optimization of battles * battle summary window -* fixed crashbug occurring sometimes on exiting battle -* confirm window is shown before retreat +* fixed crashbug occurring sometimes on exiting battle +* confirm window is shown before retreat * graphic stack queue in battle (shows when 'c' key is pressed) * it's possible to attack enemy hero * neutral monster army disappears when defeated * casualties among hero army and neutral creatures are saved * better animation handling in battles -* directional attack in battles +* directional attack in battles * mostly done battle options (although they're not saved) -* added receiving exp (and leveling-up) after a won battle -* added support for archery, offence and armourer secondary abilities +* added receiving exp (and leveling-up) after a won battle +* added support for archery, offence and armourer secondary abilities * hero's primary skills accounted for damage dealt by creatures in battle -### TOWNS: -* mostly done marketplace +### TOWNS + +* mostly done marketplace * fixed crashbug with battles on swamps and rough terrain -* counterattacks +* counterattacks * heroes can learn new spells in towns * working resource silo * fixed bug with the mage guild when no spells available * it's possible to build lighthouse -### HERO WINDOW: +### HERO WINDOW + * setting army formation * tooltips for artifacts in backpack -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * fixed bug with disappearing head of a hero in adventure map -* some objects are no longer accessible from the top +* some objects are no longer accessible from the top * no tooltips for objects under FoW * events won't be shown * working Subterranean Gates, Monoliths -* minimap shows all flaggable objects (towns, mines, etc.) +* minimap shows all flaggable objects (towns, mines, etc.) * artifacts we pick up go to the appropriate slot (if free) -# 0.61 -> 0.62 (Sep 01 2008) +## 0.61 -> 0.62 (Sep 01 2008) + +### GENERAL -### GENERAL: * restructured to the server-client model * support for heroes placed in towns * upgrading creatures @@ -3046,7 +3411,8 @@ And a lot of minor fixes * showing creature amount in the creature info window * giving starting bonus -### CASTLES: +### CASTLES + * icon in infobox showing that there is hero in town garrison * fort/citadel/castle screen * taking last stack from the heroes army should be impossible (or at least harder) @@ -3054,20 +3420,23 @@ And a lot of minor fixes * randomizing spells in towns * viewing hero window in the town screen * possibility of moving hero into the garrison -* mage guild screen +* mage guild screen * support for blacksmith * if hero doesn't have a spell book, he can buy one in a mage guild * it's possible to build glyph of fear in fortress * creatures placeholders work properly -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * hopefully fixed problems with wrong town defs (village/fort/capitol) -### HERO WINDOW: +### HERO WINDOW + * bugfix: splitting stacks works in hero window * removed bug causing significant increase of CPU consumption -### BATTLES: +### BATTLES + * shooting * removed some displaying problems * showing last group of frames in creature animation won't crash @@ -3080,20 +3449,23 @@ And a lot of minor fixes * improved pathfinding in battles, removed problems with displaying movement, adventure map interface won't be called during battles. * minor optimizations -### PREGAME: +### PREGAME + * updates settings when selecting new map after changing sorting criteria * if sorting not by name, name will be used as a secondary criteria * when filter is applied a first available map is selected automatically * slider position updated after sorting in pregame -### OBJECTS: +### OBJECTS + * support for the Tree of knowledge * support for Campfires * added event message when picking artifact -# 0.6 -> 0.61 (Jun 15 2008) +## 0.6 -> 0.61 (Jun 15 2008) + +### IMPROVEMENTS -### IMPROVEMENTS: * improved attacking in the battles * it's possible to kill hostile stack * animations won't go in the same phase @@ -3109,7 +3481,8 @@ And a lot of minor fixes * battle log is scrolled down when new event occurs * console is closed when application exits -### BUGFIXES: +### BUGFIXES + * stack at the limit of unit's range can now be attacked * good background for the town hall screen in Stronghold * fixed typo in hall.txt @@ -3119,7 +3492,7 @@ And a lot of minor fixes * properly displaying two-hex creatures in recruit/split/info window * corrupted map file won't cause crash on initializing main menu -# 0.59 -> 0.6 (Jun 1 2008) +## 0.59 -> 0.6 (Jun 1 2008) * partially done attacking in battles * screen isn't now refreshed while blitting creature info window @@ -3132,7 +3505,7 @@ And a lot of minor fixes * new pathfinder * several minor improvements -# 0.58 -> 0.59 (May 24 2008 - closed, test release) +## 0.58 -> 0.59 (May 24 2008 - closed, test release) * fixed memory leak in battles * blitting creature animations to rects in the recruitment window @@ -3152,9 +3525,10 @@ And a lot of minor fixes * callback for buttons/lists based on boost::function * a lot of minor improvements -# 0.55 -> 0.58 (Apr 20 2008 - closed, test release) +## 0.55 -> 0.58 (Apr 20 2008 - closed, test release) + +### TOWNS -### TOWNS: * recruiting creatures * working creature growths (including castle and horde building influences) * towns give income @@ -3163,21 +3537,24 @@ And a lot of minor fixes * hints for structures * updating town infobox -### GARRISONS: +### GARRISONS + * merging stacks * splitting stacks -### BATTLES: +### BATTLES + * starting battles * displaying terrain, animations of heroes, units, grid, range of units, battle menu with console, amounts of units in stacks * leaving battle by pressing flee button * moving units in battles and displaying their ranges * defend command for units -### GENERAL: +### GENERAL + * a number of minor fixes and improvements -# 0.54 -> 0.55 (Feb 29 2008) +## 0.54 -> 0.55 (Feb 29 2008) * Sprites/ folder works for h3sprite.lod same as Data/ for h3bitmap.lod (but it's still experimental) * randomization quantity of creatures on the map @@ -3190,7 +3567,8 @@ And a lot of minor fixes * hints for most of creature generators * some minor stuff -# 0.53b -> 0.54 (Feb 23 2008 - first public release) +## 0.53b -> 0.54 (Feb 23 2008 - first public release) + * given hero is placed in the town entrance * some objects such as river delta won't be blitted "on" hero * tiles under FoW are inaccessible @@ -3203,12 +3581,12 @@ And a lot of minor fixes * added hints in town lists * eliminated square from city hints -# 0.53 - 0.53b (Feb 20 2008) +## 0.53 - 0.53b (Feb 20 2008) * added giving default buildings in towns * town infobox won't crash on empty town -# 0.52 - 0.53 (Feb 18 2008): +## 0.52 - 0.53 (Feb 18 2008) * hopefully the last bugfix of Pandora's Box * fixed blockmaps of generated heroes @@ -3225,7 +3603,7 @@ And a lot of minor fixes * mostly done town infobox * town daily income is properly calculated -# 0.51 - 0.52 (Feb 7 2008): +## 0.51 - 0.52 (Feb 7 2008) * [feature] giving starting hero * [feature] VCMI will try to use files from /Data folder instead of those from h3bitmap.lod @@ -3236,7 +3614,7 @@ And a lot of minor fixes * [bugfix] improved randomization * [bugfix] pathfinder can't be cheated (what caused errors) -# 0.5 - 0.51 (Feb 3 2008): +## 0.5 - 0.51 (Feb 3 2008) * close button properly closes (same does 'q' key) * two players can't have selected same hero @@ -3249,7 +3627,7 @@ And a lot of minor fixes * better console messages * map reading speed up (though it's still slow, especially on bigger maps) -# 0.0 -> 0.5 (Feb 2 2008 - first closed release): +## 0.0 -> 0.5 (Feb 2 2008 - first closed release) * Main menu and New game screens * Scenario selection, part of advanced options support diff --git a/Mods/vcmi/Data/NotoSans-Medium.ttf b/Mods/vcmi/Content/Data/NotoSans-Medium.ttf similarity index 100% rename from Mods/vcmi/Data/NotoSans-Medium.ttf rename to Mods/vcmi/Content/Data/NotoSans-Medium.ttf diff --git a/Mods/vcmi/Data/NotoSerif-Black.ttf b/Mods/vcmi/Content/Data/NotoSerif-Black.ttf similarity index 100% rename from Mods/vcmi/Data/NotoSerif-Black.ttf rename to Mods/vcmi/Content/Data/NotoSerif-Black.ttf diff --git a/Mods/vcmi/Data/NotoSerif-Bold.ttf b/Mods/vcmi/Content/Data/NotoSerif-Bold.ttf similarity index 100% rename from Mods/vcmi/Data/NotoSerif-Bold.ttf rename to Mods/vcmi/Content/Data/NotoSerif-Bold.ttf diff --git a/Mods/vcmi/Data/NotoSerif-Medium.ttf b/Mods/vcmi/Content/Data/NotoSerif-Medium.ttf similarity index 100% rename from Mods/vcmi/Data/NotoSerif-Medium.ttf rename to Mods/vcmi/Content/Data/NotoSerif-Medium.ttf diff --git a/Mods/vcmi/Data/s/std.verm b/Mods/vcmi/Content/Data/s/std.verm similarity index 100% rename from Mods/vcmi/Data/s/std.verm rename to Mods/vcmi/Content/Data/s/std.verm diff --git a/Mods/vcmi/Data/s/testy.erm b/Mods/vcmi/Content/Data/s/testy.erm similarity index 100% rename from Mods/vcmi/Data/s/testy.erm rename to Mods/vcmi/Content/Data/s/testy.erm diff --git a/Mods/vcmi/Sounds/we5.wav b/Mods/vcmi/Content/Sounds/we5.wav similarity index 100% rename from Mods/vcmi/Sounds/we5.wav rename to Mods/vcmi/Content/Sounds/we5.wav diff --git a/Mods/vcmi/Sprites/PortraitsLarge.json b/Mods/vcmi/Content/Sprites/PortraitsLarge.json similarity index 100% rename from Mods/vcmi/Sprites/PortraitsLarge.json rename to Mods/vcmi/Content/Sprites/PortraitsLarge.json diff --git a/Mods/vcmi/Sprites/PortraitsSmall.json b/Mods/vcmi/Content/Sprites/PortraitsSmall.json similarity index 100% rename from Mods/vcmi/Sprites/PortraitsSmall.json rename to Mods/vcmi/Content/Sprites/PortraitsSmall.json diff --git a/Mods/vcmi/Data/QuickRecruitmentWindow/CreaturePurchaseCard.png b/Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png similarity index 100% rename from Mods/vcmi/Data/QuickRecruitmentWindow/CreaturePurchaseCard.png rename to Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png diff --git a/Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def b/Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def similarity index 100% rename from Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def rename to Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def diff --git a/Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def b/Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def similarity index 100% rename from Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def rename to Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def diff --git a/Mods/vcmi/Sprites/QuickRecruitmentWindow/costBackground.png b/Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/costBackground.png similarity index 100% rename from Mods/vcmi/Sprites/QuickRecruitmentWindow/costBackground.png rename to Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/costBackground.png diff --git a/Mods/vcmi/Sprites/ScSelC.json b/Mods/vcmi/Content/Sprites/ScSelC.json similarity index 100% rename from Mods/vcmi/Sprites/ScSelC.json rename to Mods/vcmi/Content/Sprites/ScSelC.json diff --git a/Mods/vcmi/Data/StackQueueLarge.png b/Mods/vcmi/Content/Sprites/StackQueueLarge.png similarity index 100% rename from Mods/vcmi/Data/StackQueueLarge.png rename to Mods/vcmi/Content/Sprites/StackQueueLarge.png diff --git a/Mods/vcmi/Data/StackQueueSmall.png b/Mods/vcmi/Content/Sprites/StackQueueSmall.png similarity index 100% rename from Mods/vcmi/Data/StackQueueSmall.png rename to Mods/vcmi/Content/Sprites/StackQueueSmall.png diff --git a/Mods/vcmi/Data/UnitMaxMovementHighlight.png b/Mods/vcmi/Content/Sprites/UnitMaxMovementHighlight.png similarity index 100% rename from Mods/vcmi/Data/UnitMaxMovementHighlight.png rename to Mods/vcmi/Content/Sprites/UnitMaxMovementHighlight.png diff --git a/Mods/vcmi/Data/UnitMovementHighlight.png b/Mods/vcmi/Content/Sprites/UnitMovementHighlight.png similarity index 100% rename from Mods/vcmi/Data/UnitMovementHighlight.png rename to Mods/vcmi/Content/Sprites/UnitMovementHighlight.png diff --git a/Mods/vcmi/Content/Sprites/battle/queueDefend.png b/Mods/vcmi/Content/Sprites/battle/queueDefend.png new file mode 100644 index 000000000..87e61b3ab Binary files /dev/null and b/Mods/vcmi/Content/Sprites/battle/queueDefend.png differ diff --git a/Mods/vcmi/Content/Sprites/battle/queueWait.png b/Mods/vcmi/Content/Sprites/battle/queueWait.png new file mode 100644 index 000000000..82572bd8e Binary files /dev/null and b/Mods/vcmi/Content/Sprites/battle/queueWait.png differ diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/empty.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/empty.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/empty.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/empty.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/fullHex.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/fullHex.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/fullHex.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/fullHex.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/left.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/left.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/left.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/left.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/leftHalf.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/leftHalf.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/leftHalf.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/leftHalf.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/top.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/top.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/top.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/top.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeft.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeft.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeft.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeft.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftCorner.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftCorner.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftCorner.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftCorner.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsRed.json similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsRed.json diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/empty.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/empty.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/empty.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/empty.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/fullHex.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/fullHex.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/fullHex.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/fullHex.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/left.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/left.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/left.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/left.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/leftHalf.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/leftHalf.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/leftHalf.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/leftHalf.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/top.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/top.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/top.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/top.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeft.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeft.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeft.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeft.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftCorner.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftCorner.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftCorner.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftCorner.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png diff --git a/Mods/vcmi/Content/Sprites/cprsmall.json b/Mods/vcmi/Content/Sprites/cprsmall.json new file mode 100644 index 000000000..3eec15da7 --- /dev/null +++ b/Mods/vcmi/Content/Sprites/cprsmall.json @@ -0,0 +1,8 @@ +{ + "images" : + [ + // Fix for swapped in H3 icons of Wight and Wraith + { "frame" : 62, "defFile" : "cprsmall.def", "defFrame" : 63}, + { "frame" : 63, "defFile" : "cprsmall.def", "defFrame" : 62} + ] +} diff --git a/Mods/vcmi/Data/debug/blocked.png b/Mods/vcmi/Content/Sprites/debug/blocked.png similarity index 100% rename from Mods/vcmi/Data/debug/blocked.png rename to Mods/vcmi/Content/Sprites/debug/blocked.png diff --git a/Mods/vcmi/Data/debug/grid.png b/Mods/vcmi/Content/Sprites/debug/grid.png similarity index 100% rename from Mods/vcmi/Data/debug/grid.png rename to Mods/vcmi/Content/Sprites/debug/grid.png diff --git a/Mods/vcmi/Data/debug/spellRange.png b/Mods/vcmi/Content/Sprites/debug/spellRange.png similarity index 100% rename from Mods/vcmi/Data/debug/spellRange.png rename to Mods/vcmi/Content/Sprites/debug/spellRange.png diff --git a/Mods/vcmi/Data/debug/visitable.png b/Mods/vcmi/Content/Sprites/debug/visitable.png similarity index 100% rename from Mods/vcmi/Data/debug/visitable.png rename to Mods/vcmi/Content/Sprites/debug/visitable.png diff --git a/Mods/vcmi/Data/heroWindow/artifactSlotEmpty.png b/Mods/vcmi/Content/Sprites/heroWindow/artifactSlotEmpty.png similarity index 100% rename from Mods/vcmi/Data/heroWindow/artifactSlotEmpty.png rename to Mods/vcmi/Content/Sprites/heroWindow/artifactSlotEmpty.png diff --git a/Mods/vcmi/Data/heroWindow/backpackButtonIcon.png b/Mods/vcmi/Content/Sprites/heroWindow/backpackButtonIcon.png similarity index 100% rename from Mods/vcmi/Data/heroWindow/backpackButtonIcon.png rename to Mods/vcmi/Content/Sprites/heroWindow/backpackButtonIcon.png diff --git a/Mods/vcmi/Data/heroWindow/commanderButtonIcon.png b/Mods/vcmi/Content/Sprites/heroWindow/commanderButtonIcon.png similarity index 100% rename from Mods/vcmi/Data/heroWindow/commanderButtonIcon.png rename to Mods/vcmi/Content/Sprites/heroWindow/commanderButtonIcon.png diff --git a/Mods/vcmi/Sprites/itpa.json b/Mods/vcmi/Content/Sprites/itpa.json similarity index 100% rename from Mods/vcmi/Sprites/itpa.json rename to Mods/vcmi/Content/Sprites/itpa.json diff --git a/Mods/vcmi/Sprites/lobby/checkbox.json b/Mods/vcmi/Content/Sprites/lobby/checkbox.json similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkbox.json rename to Mods/vcmi/Content/Sprites/lobby/checkbox.json diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png b/Mods/vcmi/Content/Sprites/lobby/checkboxBlueOff.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkboxBlueOff.png rename to Mods/vcmi/Content/Sprites/lobby/checkboxBlueOff.png diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png b/Mods/vcmi/Content/Sprites/lobby/checkboxBlueOn.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkboxBlueOn.png rename to Mods/vcmi/Content/Sprites/lobby/checkboxBlueOn.png diff --git a/Mods/vcmi/Sprites/lobby/checkboxOff.png b/Mods/vcmi/Content/Sprites/lobby/checkboxOff.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkboxOff.png rename to Mods/vcmi/Content/Sprites/lobby/checkboxOff.png diff --git a/Mods/vcmi/Sprites/lobby/checkboxOn.png b/Mods/vcmi/Content/Sprites/lobby/checkboxOn.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkboxOn.png rename to Mods/vcmi/Content/Sprites/lobby/checkboxOn.png diff --git a/Mods/vcmi/Sprites/lobby/delete-normal.png b/Mods/vcmi/Content/Sprites/lobby/delete-normal.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/delete-normal.png rename to Mods/vcmi/Content/Sprites/lobby/delete-normal.png diff --git a/Mods/vcmi/Sprites/lobby/delete-pressed.png b/Mods/vcmi/Content/Sprites/lobby/delete-pressed.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/delete-pressed.png rename to Mods/vcmi/Content/Sprites/lobby/delete-pressed.png diff --git a/Mods/vcmi/Sprites/lobby/deleteButton.json b/Mods/vcmi/Content/Sprites/lobby/deleteButton.json similarity index 100% rename from Mods/vcmi/Sprites/lobby/deleteButton.json rename to Mods/vcmi/Content/Sprites/lobby/deleteButton.json diff --git a/Mods/vcmi/Sprites/lobby/dropdown.json b/Mods/vcmi/Content/Sprites/lobby/dropdown.json similarity index 100% rename from Mods/vcmi/Sprites/lobby/dropdown.json rename to Mods/vcmi/Content/Sprites/lobby/dropdown.json diff --git a/Mods/vcmi/Sprites/lobby/dropdownNormal.png b/Mods/vcmi/Content/Sprites/lobby/dropdownNormal.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/dropdownNormal.png rename to Mods/vcmi/Content/Sprites/lobby/dropdownNormal.png diff --git a/Mods/vcmi/Sprites/lobby/dropdownPressed.png b/Mods/vcmi/Content/Sprites/lobby/dropdownPressed.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/dropdownPressed.png rename to Mods/vcmi/Content/Sprites/lobby/dropdownPressed.png diff --git a/Mods/vcmi/Data/lobby/iconFolder.png b/Mods/vcmi/Content/Sprites/lobby/iconFolder.png similarity index 100% rename from Mods/vcmi/Data/lobby/iconFolder.png rename to Mods/vcmi/Content/Sprites/lobby/iconFolder.png diff --git a/Mods/vcmi/Data/lobby/iconPlayer.png b/Mods/vcmi/Content/Sprites/lobby/iconPlayer.png similarity index 100% rename from Mods/vcmi/Data/lobby/iconPlayer.png rename to Mods/vcmi/Content/Sprites/lobby/iconPlayer.png diff --git a/Mods/vcmi/Data/lobby/iconSend.png b/Mods/vcmi/Content/Sprites/lobby/iconSend.png similarity index 100% rename from Mods/vcmi/Data/lobby/iconSend.png rename to Mods/vcmi/Content/Sprites/lobby/iconSend.png diff --git a/Mods/vcmi/Data/lobby/selectionTabSortDate.png b/Mods/vcmi/Content/Sprites/lobby/selectionTabSortDate.png similarity index 100% rename from Mods/vcmi/Data/lobby/selectionTabSortDate.png rename to Mods/vcmi/Content/Sprites/lobby/selectionTabSortDate.png diff --git a/Mods/vcmi/Data/lobby/townBorderBig.png b/Mods/vcmi/Content/Sprites/lobby/townBorderBig.png similarity index 100% rename from Mods/vcmi/Data/lobby/townBorderBig.png rename to Mods/vcmi/Content/Sprites/lobby/townBorderBig.png diff --git a/Mods/vcmi/Data/lobby/townBorderBigActivated.png b/Mods/vcmi/Content/Sprites/lobby/townBorderBigActivated.png similarity index 100% rename from Mods/vcmi/Data/lobby/townBorderBigActivated.png rename to Mods/vcmi/Content/Sprites/lobby/townBorderBigActivated.png diff --git a/Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png b/Mods/vcmi/Content/Sprites/lobby/townBorderBigGrayedOut.png similarity index 100% rename from Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png rename to Mods/vcmi/Content/Sprites/lobby/townBorderBigGrayedOut.png diff --git a/Mods/vcmi/Data/lobby/townBorderSmallActivated.png b/Mods/vcmi/Content/Sprites/lobby/townBorderSmallActivated.png similarity index 100% rename from Mods/vcmi/Data/lobby/townBorderSmallActivated.png rename to Mods/vcmi/Content/Sprites/lobby/townBorderSmallActivated.png diff --git a/Mods/vcmi/Sprites/mapFormatIcons/vcmi1.png b/Mods/vcmi/Content/Sprites/mapFormatIcons/vcmi1.png similarity index 100% rename from Mods/vcmi/Sprites/mapFormatIcons/vcmi1.png rename to Mods/vcmi/Content/Sprites/mapFormatIcons/vcmi1.png diff --git a/Mods/vcmi/Data/questDialog.png b/Mods/vcmi/Content/Sprites/questDialog.png similarity index 100% rename from Mods/vcmi/Data/questDialog.png rename to Mods/vcmi/Content/Sprites/questDialog.png diff --git a/Mods/vcmi/Data/radialMenu/altDown.png b/Mods/vcmi/Content/Sprites/radialMenu/altDown.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/altDown.png rename to Mods/vcmi/Content/Sprites/radialMenu/altDown.png diff --git a/Mods/vcmi/Data/radialMenu/altDownBottom.png b/Mods/vcmi/Content/Sprites/radialMenu/altDownBottom.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/altDownBottom.png rename to Mods/vcmi/Content/Sprites/radialMenu/altDownBottom.png diff --git a/Mods/vcmi/Data/radialMenu/altUp.png b/Mods/vcmi/Content/Sprites/radialMenu/altUp.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/altUp.png rename to Mods/vcmi/Content/Sprites/radialMenu/altUp.png diff --git a/Mods/vcmi/Data/radialMenu/altUpTop.png b/Mods/vcmi/Content/Sprites/radialMenu/altUpTop.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/altUpTop.png rename to Mods/vcmi/Content/Sprites/radialMenu/altUpTop.png diff --git a/Mods/vcmi/Data/radialMenu/dismissHero.png b/Mods/vcmi/Content/Sprites/radialMenu/dismissHero.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/dismissHero.png rename to Mods/vcmi/Content/Sprites/radialMenu/dismissHero.png diff --git a/Mods/vcmi/Data/radialMenu/heroMove.png b/Mods/vcmi/Content/Sprites/radialMenu/heroMove.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/heroMove.png rename to Mods/vcmi/Content/Sprites/radialMenu/heroMove.png diff --git a/Mods/vcmi/Data/radialMenu/heroSwap.png b/Mods/vcmi/Content/Sprites/radialMenu/heroSwap.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/heroSwap.png rename to Mods/vcmi/Content/Sprites/radialMenu/heroSwap.png diff --git a/Mods/vcmi/Data/radialMenu/itemEmpty.png b/Mods/vcmi/Content/Sprites/radialMenu/itemEmpty.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/itemEmpty.png rename to Mods/vcmi/Content/Sprites/radialMenu/itemEmpty.png diff --git a/Mods/vcmi/Data/radialMenu/itemEmptyAlt.png b/Mods/vcmi/Content/Sprites/radialMenu/itemEmptyAlt.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/itemEmptyAlt.png rename to Mods/vcmi/Content/Sprites/radialMenu/itemEmptyAlt.png diff --git a/Mods/vcmi/Data/radialMenu/itemInactive.png b/Mods/vcmi/Content/Sprites/radialMenu/itemInactive.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/itemInactive.png rename to Mods/vcmi/Content/Sprites/radialMenu/itemInactive.png diff --git a/Mods/vcmi/Data/radialMenu/itemInactiveAlt.png b/Mods/vcmi/Content/Sprites/radialMenu/itemInactiveAlt.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/itemInactiveAlt.png rename to Mods/vcmi/Content/Sprites/radialMenu/itemInactiveAlt.png diff --git a/Mods/vcmi/Data/radialMenu/moveArtifacts.png b/Mods/vcmi/Content/Sprites/radialMenu/moveArtifacts.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/moveArtifacts.png rename to Mods/vcmi/Content/Sprites/radialMenu/moveArtifacts.png diff --git a/Mods/vcmi/Data/radialMenu/moveTroops.png b/Mods/vcmi/Content/Sprites/radialMenu/moveTroops.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/moveTroops.png rename to Mods/vcmi/Content/Sprites/radialMenu/moveTroops.png diff --git a/Mods/vcmi/Data/radialMenu/stackFillOne.png b/Mods/vcmi/Content/Sprites/radialMenu/stackFillOne.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackFillOne.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackFillOne.png diff --git a/Mods/vcmi/Data/radialMenu/stackMerge.png b/Mods/vcmi/Content/Sprites/radialMenu/stackMerge.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackMerge.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackMerge.png diff --git a/Mods/vcmi/Data/radialMenu/stackSplitDialog.png b/Mods/vcmi/Content/Sprites/radialMenu/stackSplitDialog.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackSplitDialog.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackSplitDialog.png diff --git a/Mods/vcmi/Data/radialMenu/stackSplitEqual.png b/Mods/vcmi/Content/Sprites/radialMenu/stackSplitEqual.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackSplitEqual.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackSplitEqual.png diff --git a/Mods/vcmi/Data/radialMenu/stackSplitOne.png b/Mods/vcmi/Content/Sprites/radialMenu/stackSplitOne.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackSplitOne.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackSplitOne.png diff --git a/Mods/vcmi/Data/radialMenu/statusBar.png b/Mods/vcmi/Content/Sprites/radialMenu/statusBar.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/statusBar.png rename to Mods/vcmi/Content/Sprites/radialMenu/statusBar.png diff --git a/Mods/vcmi/Data/radialMenu/swapArtifacts.png b/Mods/vcmi/Content/Sprites/radialMenu/swapArtifacts.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/swapArtifacts.png rename to Mods/vcmi/Content/Sprites/radialMenu/swapArtifacts.png diff --git a/Mods/vcmi/Data/radialMenu/tradeHeroes.png b/Mods/vcmi/Content/Sprites/radialMenu/tradeHeroes.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/tradeHeroes.png rename to Mods/vcmi/Content/Sprites/radialMenu/tradeHeroes.png diff --git a/Mods/vcmi/Data/settingsWindow/frameAudio.png b/Mods/vcmi/Content/Sprites/settingsWindow/frameAudio.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/frameAudio.png rename to Mods/vcmi/Content/Sprites/settingsWindow/frameAudio.png diff --git a/Mods/vcmi/Data/settingsWindow/frameMovement.png b/Mods/vcmi/Content/Sprites/settingsWindow/frameMovement.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/frameMovement.png rename to Mods/vcmi/Content/Sprites/settingsWindow/frameMovement.png diff --git a/Mods/vcmi/Data/settingsWindow/frameStackQueue.png b/Mods/vcmi/Content/Sprites/settingsWindow/frameStackQueue.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/frameStackQueue.png rename to Mods/vcmi/Content/Sprites/settingsWindow/frameStackQueue.png diff --git a/Mods/vcmi/Content/Sprites/settingsWindow/gear.png b/Mods/vcmi/Content/Sprites/settingsWindow/gear.png new file mode 100644 index 000000000..d59b548c7 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/settingsWindow/gear.png differ diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed1.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed1.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed1.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed1.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed2.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed2.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed2.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed2.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed3.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed3.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed3.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed3.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed4.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed4.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed4.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed4.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed5.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed5.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed5.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed5.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed6.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed6.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed6.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed6.png diff --git a/Mods/vcmi/Data/spellResearch/accept.png b/Mods/vcmi/Content/Sprites/spellResearch/accept.png similarity index 100% rename from Mods/vcmi/Data/spellResearch/accept.png rename to Mods/vcmi/Content/Sprites/spellResearch/accept.png diff --git a/Mods/vcmi/Data/spellResearch/close.png b/Mods/vcmi/Content/Sprites/spellResearch/close.png similarity index 100% rename from Mods/vcmi/Data/spellResearch/close.png rename to Mods/vcmi/Content/Sprites/spellResearch/close.png diff --git a/Mods/vcmi/Data/spellResearch/reroll.png b/Mods/vcmi/Content/Sprites/spellResearch/reroll.png similarity index 100% rename from Mods/vcmi/Data/spellResearch/reroll.png rename to Mods/vcmi/Content/Sprites/spellResearch/reroll.png diff --git a/Mods/vcmi/Content/Sprites/spells.json b/Mods/vcmi/Content/Sprites/spells.json new file mode 100644 index 000000000..4c13e5c79 --- /dev/null +++ b/Mods/vcmi/Content/Sprites/spells.json @@ -0,0 +1,8 @@ +{ + "images" : + [ + // Fix for swapped in H3 icons of View Earth and View Air + { "frame" : 3, "defFile" : "spells.def", "defFrame" : 5}, + { "frame" : 5, "defFile" : "spells.def", "defFrame" : 3} + ] +} diff --git a/Mods/vcmi/Data/stackWindow/bonus-effects.png b/Mods/vcmi/Content/Sprites/stackWindow/bonus-effects.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/bonus-effects.png rename to Mods/vcmi/Content/Sprites/stackWindow/bonus-effects.png diff --git a/Mods/vcmi/Data/stackWindow/button-panel.png b/Mods/vcmi/Content/Sprites/stackWindow/button-panel.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/button-panel.png rename to Mods/vcmi/Content/Sprites/stackWindow/button-panel.png diff --git a/Mods/vcmi/Sprites/stackWindow/cancel-normal.png b/Mods/vcmi/Content/Sprites/stackWindow/cancel-normal.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/cancel-normal.png rename to Mods/vcmi/Content/Sprites/stackWindow/cancel-normal.png diff --git a/Mods/vcmi/Sprites/stackWindow/cancel-pressed.png b/Mods/vcmi/Content/Sprites/stackWindow/cancel-pressed.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/cancel-pressed.png rename to Mods/vcmi/Content/Sprites/stackWindow/cancel-pressed.png diff --git a/Mods/vcmi/Sprites/stackWindow/cancelButton.json b/Mods/vcmi/Content/Sprites/stackWindow/cancelButton.json similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/cancelButton.json rename to Mods/vcmi/Content/Sprites/stackWindow/cancelButton.json diff --git a/Mods/vcmi/Data/stackWindow/commander-abilities.png b/Mods/vcmi/Content/Sprites/stackWindow/commander-abilities.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/commander-abilities.png rename to Mods/vcmi/Content/Sprites/stackWindow/commander-abilities.png diff --git a/Mods/vcmi/Data/stackWindow/commander-bg.png b/Mods/vcmi/Content/Sprites/stackWindow/commander-bg.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/commander-bg.png rename to Mods/vcmi/Content/Sprites/stackWindow/commander-bg.png diff --git a/Mods/vcmi/Data/stackWindow/icons.png b/Mods/vcmi/Content/Sprites/stackWindow/icons.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/icons.png rename to Mods/vcmi/Content/Sprites/stackWindow/icons.png diff --git a/Mods/vcmi/Data/stackWindow/info-panel-0.png b/Mods/vcmi/Content/Sprites/stackWindow/info-panel-0.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/info-panel-0.png rename to Mods/vcmi/Content/Sprites/stackWindow/info-panel-0.png diff --git a/Mods/vcmi/Data/stackWindow/info-panel-1.png b/Mods/vcmi/Content/Sprites/stackWindow/info-panel-1.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/info-panel-1.png rename to Mods/vcmi/Content/Sprites/stackWindow/info-panel-1.png diff --git a/Mods/vcmi/Data/stackWindow/info-panel-2.png b/Mods/vcmi/Content/Sprites/stackWindow/info-panel-2.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/info-panel-2.png rename to Mods/vcmi/Content/Sprites/stackWindow/info-panel-2.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-0.png b/Mods/vcmi/Content/Sprites/stackWindow/level-0.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-0.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-0.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-1.png b/Mods/vcmi/Content/Sprites/stackWindow/level-1.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-1.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-1.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-10.png b/Mods/vcmi/Content/Sprites/stackWindow/level-10.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-10.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-10.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-2.png b/Mods/vcmi/Content/Sprites/stackWindow/level-2.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-2.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-2.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-3.png b/Mods/vcmi/Content/Sprites/stackWindow/level-3.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-3.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-3.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-4.png b/Mods/vcmi/Content/Sprites/stackWindow/level-4.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-4.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-4.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-5.png b/Mods/vcmi/Content/Sprites/stackWindow/level-5.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-5.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-5.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-6.png b/Mods/vcmi/Content/Sprites/stackWindow/level-6.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-6.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-6.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-7.png b/Mods/vcmi/Content/Sprites/stackWindow/level-7.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-7.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-7.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-8.png b/Mods/vcmi/Content/Sprites/stackWindow/level-8.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-8.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-8.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-9.png b/Mods/vcmi/Content/Sprites/stackWindow/level-9.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-9.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-9.png diff --git a/Mods/vcmi/Sprites/stackWindow/levels.json b/Mods/vcmi/Content/Sprites/stackWindow/levels.json similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/levels.json rename to Mods/vcmi/Content/Sprites/stackWindow/levels.json diff --git a/Mods/vcmi/Data/stackWindow/spell-effects.png b/Mods/vcmi/Content/Sprites/stackWindow/spell-effects.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/spell-effects.png rename to Mods/vcmi/Content/Sprites/stackWindow/spell-effects.png diff --git a/Mods/vcmi/Sprites/stackWindow/switchModeIcons.json b/Mods/vcmi/Content/Sprites/stackWindow/switchModeIcons.json similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/switchModeIcons.json rename to Mods/vcmi/Content/Sprites/stackWindow/switchModeIcons.json diff --git a/Mods/vcmi/Sprites/stackWindow/upgrade-normal.png b/Mods/vcmi/Content/Sprites/stackWindow/upgrade-normal.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/upgrade-normal.png rename to Mods/vcmi/Content/Sprites/stackWindow/upgrade-normal.png diff --git a/Mods/vcmi/Sprites/stackWindow/upgrade-pressed.png b/Mods/vcmi/Content/Sprites/stackWindow/upgrade-pressed.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/upgrade-pressed.png rename to Mods/vcmi/Content/Sprites/stackWindow/upgrade-pressed.png diff --git a/Mods/vcmi/Sprites/stackWindow/upgradeButton.json b/Mods/vcmi/Content/Sprites/stackWindow/upgradeButton.json similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/upgradeButton.json rename to Mods/vcmi/Content/Sprites/stackWindow/upgradeButton.json diff --git a/Mods/vcmi/Sprites/vcmi/creatureIcons/towerLarge.png b/Mods/vcmi/Content/Sprites/vcmi/creatureIcons/towerLarge.png similarity index 100% rename from Mods/vcmi/Sprites/vcmi/creatureIcons/towerLarge.png rename to Mods/vcmi/Content/Sprites/vcmi/creatureIcons/towerLarge.png diff --git a/Mods/vcmi/Sprites/vcmi/creatureIcons/towerSmall.png b/Mods/vcmi/Content/Sprites/vcmi/creatureIcons/towerSmall.png similarity index 100% rename from Mods/vcmi/Sprites/vcmi/creatureIcons/towerSmall.png rename to Mods/vcmi/Content/Sprites/vcmi/creatureIcons/towerSmall.png diff --git a/Mods/vcmi/Content/Sprites2x/battle/queueDefend.png b/Mods/vcmi/Content/Sprites2x/battle/queueDefend.png new file mode 100644 index 000000000..7ba9c0a58 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/battle/queueDefend.png differ diff --git a/Mods/vcmi/Content/Sprites2x/battle/queueWait.png b/Mods/vcmi/Content/Sprites2x/battle/queueWait.png new file mode 100644 index 000000000..53e203dda Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/battle/queueWait.png differ diff --git a/Mods/vcmi/Content/Sprites2x/mapFormatIcons/vcmi1.png b/Mods/vcmi/Content/Sprites2x/mapFormatIcons/vcmi1.png new file mode 100644 index 000000000..1be03a460 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/mapFormatIcons/vcmi1.png differ diff --git a/Mods/vcmi/Content/Sprites2x/settingsWindow/gear.png b/Mods/vcmi/Content/Sprites2x/settingsWindow/gear.png new file mode 100644 index 000000000..ff89fc0e6 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/settingsWindow/gear.png differ diff --git a/Mods/vcmi/Content/Sprites2x/stackWindow/icons.png b/Mods/vcmi/Content/Sprites2x/stackWindow/icons.png new file mode 100644 index 000000000..b1cdf1c22 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/stackWindow/icons.png differ diff --git a/Mods/vcmi/Content/Sprites3x/battle/queueDefend.png b/Mods/vcmi/Content/Sprites3x/battle/queueDefend.png new file mode 100644 index 000000000..1740d5b16 Binary files /dev/null and b/Mods/vcmi/Content/Sprites3x/battle/queueDefend.png differ diff --git a/Mods/vcmi/Content/Sprites3x/mapFormatIcons/vcmi1.png b/Mods/vcmi/Content/Sprites3x/mapFormatIcons/vcmi1.png new file mode 100644 index 000000000..d85607d13 Binary files /dev/null and b/Mods/vcmi/Content/Sprites3x/mapFormatIcons/vcmi1.png differ diff --git a/Mods/vcmi/Content/Sprites3x/settingsWindow/gear.png b/Mods/vcmi/Content/Sprites3x/settingsWindow/gear.png new file mode 100644 index 000000000..93cc18eeb Binary files /dev/null and b/Mods/vcmi/Content/Sprites3x/settingsWindow/gear.png differ diff --git a/Mods/vcmi/Content/Sprites3x/stackWindow/icons.png b/Mods/vcmi/Content/Sprites3x/stackWindow/icons.png new file mode 100644 index 000000000..3ec5a9e32 Binary files /dev/null and b/Mods/vcmi/Content/Sprites3x/stackWindow/icons.png differ diff --git a/Mods/vcmi/Content/Sprites4x/battle/queueDefend.png b/Mods/vcmi/Content/Sprites4x/battle/queueDefend.png new file mode 100644 index 000000000..df4a21235 Binary files /dev/null and b/Mods/vcmi/Content/Sprites4x/battle/queueDefend.png differ diff --git a/Mods/vcmi/Content/Sprites4x/mapFormatIcons/vcmi1.png b/Mods/vcmi/Content/Sprites4x/mapFormatIcons/vcmi1.png new file mode 100644 index 000000000..0193036be Binary files /dev/null and b/Mods/vcmi/Content/Sprites4x/mapFormatIcons/vcmi1.png differ diff --git a/Mods/vcmi/Content/Sprites4x/settingsWindow/gear.png b/Mods/vcmi/Content/Sprites4x/settingsWindow/gear.png new file mode 100644 index 000000000..83d451dbe Binary files /dev/null and b/Mods/vcmi/Content/Sprites4x/settingsWindow/gear.png differ diff --git a/Mods/vcmi/Content/Sprites4x/stackWindow/icons.png b/Mods/vcmi/Content/Sprites4x/stackWindow/icons.png new file mode 100644 index 000000000..95abe6c3c Binary files /dev/null and b/Mods/vcmi/Content/Sprites4x/stackWindow/icons.png differ diff --git a/Mods/vcmi/Video/tutorial/AbortSpell.webm b/Mods/vcmi/Content/Video/tutorial/AbortSpell.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/AbortSpell.webm rename to Mods/vcmi/Content/Video/tutorial/AbortSpell.webm diff --git a/Mods/vcmi/Video/tutorial/BattleDirection.webm b/Mods/vcmi/Content/Video/tutorial/BattleDirection.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/BattleDirection.webm rename to Mods/vcmi/Content/Video/tutorial/BattleDirection.webm diff --git a/Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm b/Mods/vcmi/Content/Video/tutorial/BattleDirectionAbort.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm rename to Mods/vcmi/Content/Video/tutorial/BattleDirectionAbort.webm diff --git a/Mods/vcmi/Video/tutorial/MapPanning.webm b/Mods/vcmi/Content/Video/tutorial/MapPanning.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/MapPanning.webm rename to Mods/vcmi/Content/Video/tutorial/MapPanning.webm diff --git a/Mods/vcmi/Video/tutorial/MapZooming.webm b/Mods/vcmi/Content/Video/tutorial/MapZooming.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/MapZooming.webm rename to Mods/vcmi/Content/Video/tutorial/MapZooming.webm diff --git a/Mods/vcmi/Video/tutorial/RadialWheel.webm b/Mods/vcmi/Content/Video/tutorial/RadialWheel.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/RadialWheel.webm rename to Mods/vcmi/Content/Video/tutorial/RadialWheel.webm diff --git a/Mods/vcmi/Video/tutorial/RightClick.webm b/Mods/vcmi/Content/Video/tutorial/RightClick.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/RightClick.webm rename to Mods/vcmi/Content/Video/tutorial/RightClick.webm diff --git a/Mods/vcmi/config/chinese.json b/Mods/vcmi/Content/config/chinese.json similarity index 95% rename from Mods/vcmi/config/chinese.json rename to Mods/vcmi/Content/config/chinese.json index 67605102e..ed93bb447 100644 --- a/Mods/vcmi/config/chinese.json +++ b/Mods/vcmi/Content/config/chinese.json @@ -11,7 +11,7 @@ "vcmi.adventureMap.monsterThreat.levels.8" : "挑战性的", "vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的", "vcmi.adventureMap.monsterThreat.levels.10" : "致命的", - "vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜", + "vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜的", "vcmi.adventureMap.monsterLevel" : "\n\n%TOWN%LEVEL级%ATTACK_TYPE生物", "vcmi.adventureMap.monsterMeleeType" : "近战", "vcmi.adventureMap.monsterRangedType" : "远程", @@ -28,6 +28,13 @@ "vcmi.adventureMap.movementPointsHeroInfo" : "(移动点数: %REMAINING / %POINTS)", "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "抱歉,重放对手行动功能目前暂未实现!", + "vcmi.bonusSource.artifact" : "宝物", + "vcmi.bonusSource.creature" : "技能", + "vcmi.bonusSource.spell" : "法术", + "vcmi.bonusSource.hero" : "英雄", + "vcmi.bonusSource.commander" : "指挥官", + "vcmi.bonusSource.other" : "其他", + "vcmi.capitalColors.0" : "红色", "vcmi.capitalColors.1" : "蓝色", "vcmi.capitalColors.2" : "褐色", @@ -107,6 +114,12 @@ "vcmi.lobby.handicap.resource" : "给予玩家起始资源以外的更多资源,允许负值,但总量不会低于0(玩家永远不会能以负资源开始游戏)。", "vcmi.lobby.handicap.income" : "按百分比改变玩家的各种收入,向上取整。", "vcmi.lobby.handicap.growth" : "改变玩家拥有的城镇的生物增长率,向上取整。", + "vcmi.lobby.deleteUnsupportedSave" : "{检测到无法支持的存档}\n\nVCMI检测到%d个存档已不再受支持,这可能是由于 VCMI 版本不兼容导致的。\n\n你是否要删除这些存档?", + "vcmi.lobby.deleteSaveGameTitle" : "选择一个要删除的存档", + "vcmi.lobby.deleteMapTitle" : "选择一个要删除要删除的场景", + "vcmi.lobby.deleteFile" : "你确定要删除下列文件?", + "vcmi.lobby.deleteFolder" : "你确定要删除下列文件夹?", + "vcmi.lobby.deleteMode" : "切换删除模式并返回", "vcmi.lobby.login.title" : "VCMI大厅", "vcmi.lobby.login.username" : "用户名:", @@ -175,9 +188,6 @@ "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", "vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}", "vcmi.server.errors.modsToDisable" : "{需要禁用的mod列表}", - "vcmi.server.errors.modNoDependency" : "读取mod包 {'%s'}失败!\n 需要的mod {'%s'} 没有安装或无效!\n", - "vcmi.server.errors.modDependencyLoop" : "读取mod包 {'%s'}失败!\n 这个mod可能存在循环(软)依赖!", - "vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n", "vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!", "vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。", @@ -291,7 +301,7 @@ "vcmi.battleOptions.queueSizeNoneButton.help": "不显示回合顺序指示器", "vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏的分辨率自动调整回合顺序指示器的大小(游戏处于高度低于700像素的分辨率时,使用小,否则使用大)", "vcmi.battleOptions.queueSizeSmallButton.help": "设置回合顺序指示器为小", - "vcmi.battleOptions.queueSizeBigButton.help": "设置次寻条为大尺寸(无法在游戏高度像素低于700时生效)", + "vcmi.battleOptions.queueSizeBigButton.help": "设置回合顺序指示器为大尺寸(无法在游戏高度像素低于700时生效)", "vcmi.battleOptions.animationsSpeed1.hover": "", "vcmi.battleOptions.animationsSpeed5.hover": "", "vcmi.battleOptions.animationsSpeed6.hover": "", @@ -371,7 +381,7 @@ "vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面", "vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物", "vcmi.heroWindow.sortBackpackByCost.hover" : "按价格排序", - "vcmi.heroWindow.sortBackpackByCost.help" : "将行囊里的宝物按价格排序。.", + "vcmi.heroWindow.sortBackpackByCost.help" : "将行囊里的宝物按价格排序。", "vcmi.heroWindow.sortBackpackBySlot.hover" : "按装备槽排序", "vcmi.heroWindow.sortBackpackBySlot.help" : "将行囊里的宝物按装备槽排序。", "vcmi.heroWindow.sortBackpackByClass.hover" : "按类型排序", @@ -554,6 +564,8 @@ "core.seerhut.quest.reachDate.visit.4" : "关门直到%s。", "core.seerhut.quest.reachDate.visit.5" : "关门直到%s。", + "mapObject.core.hillFort.object.description" : "升级生物,1-4级生物升级比城镇中更便宜。", + "core.bonus.ADDITIONAL_ATTACK.name": "双击", "core.bonus.ADDITIONAL_ATTACK.description": "生物可以攻击两次", "core.bonus.ADDITIONAL_RETALIATION.name": "额外反击", @@ -709,5 +721,27 @@ "core.bonus.MECHANICAL.name": "机械", "core.bonus.MECHANICAL.description": "免疫大多数效果,可修复", "core.bonus.PRISM_HEX_ATTACK_BREATH.name": "棱光吐息", - "core.bonus.PRISM_HEX_ATTACK_BREATH.description": "攻击后向三方向扩散攻击" + "core.bonus.PRISM_HEX_ATTACK_BREATH.description": "攻击后向三方向扩散攻击", + + "spell.core.castleMoat.name" : "护城河", + "spell.core.castleMoatTrigger.name" : "护城河", + "spell.core.catapultShot.name" : "投石车射击", + "spell.core.cyclopsShot.name" : "攻城射击", + "spell.core.dungeonMoat.name" : "极热之油", + "spell.core.dungeonMoatTrigger.name" : "极热之油", + "spell.core.fireWallTrigger.name" : "烈火魔墙", + "spell.core.firstAid.name" : "急救术", + "spell.core.fortressMoat.name" : "焦油", + "spell.core.fortressMoatTrigger.name" : "焦油", + "spell.core.infernoMoat.name" : "熔岩", + "spell.core.infernoMoatTrigger.name" : "熔岩", + "spell.core.landMineTrigger.name" : "埋设地雷", + "spell.core.necropolisMoat.name" : "尸骨堆", + "spell.core.necropolisMoatTrigger.name" : "尸骨堆", + "spell.core.rampartMoat.name" : "护城河", + "spell.core.rampartMoatTrigger.name" : "护城河", + "spell.core.strongholdMoat.name" : "栅栏", + "spell.core.strongholdMoatTrigger.name" : "栅栏", + "spell.core.summonDemons.name" : "召唤恶鬼", + "spell.core.towerMoat.name" : "埋设地雷" } diff --git a/Mods/vcmi/config/czech.json b/Mods/vcmi/Content/config/czech.json similarity index 66% rename from Mods/vcmi/config/czech.json rename to Mods/vcmi/Content/config/czech.json index 8d4911f58..196a5405d 100644 --- a/Mods/vcmi/config/czech.json +++ b/Mods/vcmi/Content/config/czech.json @@ -86,12 +86,13 @@ "vcmi.spellBook.search" : "Hledat", - "vcmi.spellResearch.canNotAfford" : "Nemáte dostatek prostředků k nahrazení {%SPELL1} za {%SPELL2}. Stále však můžete toto kouzlo zrušit a pokračovat ve výzkumu kouzel.", + "vcmi.spellResearch.canNotAfford" : "Nemáte dostatek prostředků k nahrazení {%SPELL1} za {%SPELL2}. Stále však můžete toto kouzlo zrušit a pokračovat ve výzkumu dalších kouzel.", "vcmi.spellResearch.comeAgain" : "Výzkum už byl dnes proveden. Vraťte se zítra.", - "vcmi.spellResearch.pay" : "Chcete nahradit {%SPELL1} za {%SPELL2}? Nebo zrušit toto kouzlo a pokračovat ve výzkumu kouzel?", + "vcmi.spellResearch.pay" : "Chcete nahradit {%SPELL1} za {%SPELL2}? Nebo zrušit toto kouzlo a pokračovat ve výzkumu dalších kouzel?", "vcmi.spellResearch.research" : "Prozkoumat toto kouzlo", "vcmi.spellResearch.skip" : "Přeskočit toto kouzlo", "vcmi.spellResearch.abort" : "Přerušit", + "vcmi.spellResearch.noMoreSpells" : "Žádná další kouzla k výzkumu nejsou dostupná.", "vcmi.mainMenu.serverConnecting" : "Připojování...", "vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:", @@ -113,13 +114,51 @@ "vcmi.lobby.handicap.resource" : "Dává hráčům odpovídající zdroje navíc k běžným startovním zdrojům. Jsou povoleny záporné hodnoty, ale jsou omezeny na celkovou hodnotu 0 (hráč nikdy nezačíná se zápornými zdroji).", "vcmi.lobby.handicap.income" : "Mění různé příjmy hráče podle procent. Výsledek je zaokrouhlen nahoru.", "vcmi.lobby.handicap.growth" : "Mění rychlost růstu jednotel v městech vlastněných hráčem. Výsledek je zaokrouhlen nahoru.", - "vcmi.lobby.deleteUnsupportedSave" : "Nalezeny nepodporované uložené hry (např. z předchozích verzí).\n\nChcete je odstranit?", + "vcmi.lobby.deleteUnsupportedSave" : "Nalezeny nepodporované uložené hry.\n\nBylo nalezeno %d uložených her, které již nejsou podporovány, pravděpodobně kvůli rozdílům mezi verzemi VCMI.\n\nChcete je odstranit?", "vcmi.lobby.deleteSaveGameTitle" : "Vyberte uloženou hru k odstranění", "vcmi.lobby.deleteMapTitle" : "Vyberte scénář k odstranění", - "vcmi.lobby.deleteFile" : "Chcete smazat následující soubor?", - "vcmi.lobby.deleteFolder" : "Chcete smazat následující složku?", + "vcmi.lobby.deleteFile" : "Chcete odstranit následující soubor?", + "vcmi.lobby.deleteFolder" : "Chcete odstranit následující složku?", "vcmi.lobby.deleteMode" : "Přepnout do režimu mazání a zpět", + "vcmi.broadcast.failedLoadGame" : "Nepodařilo se načíst hru", + "vcmi.broadcast.command" : "Použijte '!help' pro zobrazení dostupných příkazů", + "vcmi.broadcast.simturn.end" : "Současné tahy byly ukončeny", + "vcmi.broadcast.simturn.endBetween" : "Současné tahy mezi hráči %s a %s byly ukončeny", + "vcmi.broadcast.serverProblem" : "Server narazil na problém", + "vcmi.broadcast.gameTerminated" : "Hra byla ukončena", + "vcmi.broadcast.gameSavedAs" : "Hra byla uložena jako", + "vcmi.broadcast.noCheater" : "Nejsou zaznamenáni žádní podvodníci!", + "vcmi.broadcast.playerCheater" : "Hráč %s je podvodník!", + "vcmi.broadcast.statisticFile" : "Soubory se statistikou lze nalézt v adresáři %s", + "vcmi.broadcast.help.commands" : "Dostupné příkazy pro hostitele:", + "vcmi.broadcast.help.exit" : "'!exit' - okamžitě ukončí aktuální hru", + "vcmi.broadcast.help.kick" : "'!kick ' - vyhodí vybraného hráče ze hry", + "vcmi.broadcast.help.save" : "'!save ' - uloží hru pod zadaným názvem", + "vcmi.broadcast.help.statistic" : "'!statistic' - uloží statistiky hry jako soubor CSV", + "vcmi.broadcast.help.commandsAll" : "Dostupné příkazy pro všechny hráče:", + "vcmi.broadcast.help.help" : "'!help' - zobrazí tuto nápovědu", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - zobrazí seznam hráčů, kteří během hry použili cheaty", + "vcmi.broadcast.help.vote" : "'!vote' - umožňuje změnit některá nastavení hry, pokud všichni hráči souhlasí", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - povolí současné tahy na určený počet dní nebo dokud nenastane kontakt", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - vynutí současné tahy na určený počet dní s blokováním kontaktů hráčů", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - ukončí současné tahy po skončení aktuálního tahu", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prodlouží základní časovač pro všechny hráče o určený počet sekund", + "vcmi.broadcast.vote.noActive" : "Žádné aktivní hlasování!", + "vcmi.broadcast.vote.yes" : "ano", + "vcmi.broadcast.vote.no" : "ne", + "vcmi.broadcast.vote.notRecognized" : "Hlasovací příkaz nebyl rozpoznán!", + "vcmi.broadcast.vote.success.untilContacts" : "Hlasování bylo úspěšné. Současné tahy poběží ještě %s dní nebo dokud nenastane kontakt", + "vcmi.broadcast.vote.success.contactsBlocked" : "Hlasování bylo úspěšné. Současné tahy poběží ještě %s dní. Kontakty jsou blokovány", + "vcmi.broadcast.vote.success.nextDay" : "Hlasování bylo úspěšné. Současné tahy skončí následující den", + "vcmi.broadcast.vote.success.timer" : "Hlasování bylo úspěšné. Časovač pro všechny hráče byl prodloužen o %s sekund", + "vcmi.broadcast.vote.aborted" : "Hráč hlasoval proti změně. Hlasování bylo ukončeno", + "vcmi.broadcast.vote.start.untilContacts" : "Bylo zahájeno hlasování o povolení současných tahů na %s dní", + "vcmi.broadcast.vote.start.contactsBlocked" : "Bylo zahájeno hlasování o vynucení současných tahů na %s dní", + "vcmi.broadcast.vote.start.nextDay" : "Bylo zahájeno hlasování o ukončení současných tahů od následujícího dne", + "vcmi.broadcast.vote.start.timer" : "Bylo zahájeno hlasování o prodloužení časovače pro všechny hráče o %s sekund", + "vcmi.broadcast.vote.hint" : "Napište '!vote yes', pokud souhlasíte se změnou, nebo '!vote no', pokud jste proti", + "vcmi.lobby.login.title" : "Online lobby VCMI", "vcmi.lobby.login.username" : "Uživatelské jméno:", "vcmi.lobby.login.connecting" : "Připojování...", @@ -127,6 +166,7 @@ "vcmi.lobby.login.create" : "Nový účet", "vcmi.lobby.login.login" : "Přihlásit se", "vcmi.lobby.login.as" : "Přihlásit se jako %s", + "vcmi.lobby.login.spectator" : "Divák", "vcmi.lobby.header.rooms" : "Herní místnosti - %d", "vcmi.lobby.header.channels" : "Kanály konverzace", "vcmi.lobby.header.chat.global" : "Globální konverzace hry - %s", // %s -> language name @@ -187,10 +227,9 @@ "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", "vcmi.server.errors.modsToEnable" : "{Následující modifikace jsou nutné pro načtení hry}", "vcmi.server.errors.modsToDisable" : "{Následující modifikace musí být zakázány}", - "vcmi.server.errors.modNoDependency" : "Nelze načíst modifikaci {'%s'}!\n Závisí na modifikaci {'%s'}, která není aktivní!\n", - "vcmi.server.errors.modDependencyLoop" : "Nelze načíst modifikaci {'%s'}!\n Modifikace může být součástí (nepřímé) závislostní smyčky.", - "vcmi.server.errors.modConflict" : "Nelze načíst modifikaci {'%s'}!\n Je v kolizi s aktivní modifikací {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!", + "vcmi.server.errors.wrongIdentified" : "Byli jste identifikováni jako hráč %s, zatímco byl očekáván hráč %s.", + "vcmi.server.errors.notAllowed" : "Nemáte oprávnění provést tuto akci!", "vcmi.dimensionDoor.seaToLandError" : "Pomocí dimenzní brány není možné se teleportovat z moře na pevninu nebo naopak.", @@ -286,42 +325,42 @@ "vcmi.adventureOptions.smoothDragging.help" : "{Plynulé posouvání mapy}\n\nPokud je tato možnost aktivována, posouvání mapy bude plynulé.", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Přeskočit efekty mizení", "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Přeskočit efekty mizení}\n\nKdyž je povoleno, přeskočí se efekty mizení objektů a podobné efekty (sběr surovin, nalodění atd.). V některých případech zrychlí uživatelské rozhraní na úkor estetiky. Obzvláště užitečné v PvP hrách. Pro maximální rychlost pohybu je toto nastavení aktivní bez ohledu na další volby.", - "vcmi.adventureOptions.mapScrollSpeed1.hover": "", - "vcmi.adventureOptions.mapScrollSpeed5.hover": "", - "vcmi.adventureOptions.mapScrollSpeed6.hover": "", - "vcmi.adventureOptions.mapScrollSpeed1.help": "Nastavit posouvání mapy na velmi pomalé", - "vcmi.adventureOptions.mapScrollSpeed5.help": "Nastavit posouvání mapy na velmi rychlé", - "vcmi.adventureOptions.mapScrollSpeed6.help": "Nastavit posouvání mapy na okamžité", + "vcmi.adventureOptions.mapScrollSpeed1.hover" : "", + "vcmi.adventureOptions.mapScrollSpeed5.hover" : "", + "vcmi.adventureOptions.mapScrollSpeed6.hover" : "", + "vcmi.adventureOptions.mapScrollSpeed1.help" : "Nastavit posouvání mapy na velmi pomalé", + "vcmi.adventureOptions.mapScrollSpeed5.help" : "Nastavit posouvání mapy na velmi rychlé", + "vcmi.adventureOptions.mapScrollSpeed6.help" : "Nastavit posouvání mapy na okamžité", "vcmi.adventureOptions.hideBackground.hover" : "Skrýt pozadí", "vcmi.adventureOptions.hideBackground.help" : "{Skrýt pozadí}\n\nSkryje mapu dobrodružství na pozadí a místo ní zobrazí texturu.", - "vcmi.battleOptions.queueSizeLabel.hover": "Zobrazit frontu pořadí tahů", - "vcmi.battleOptions.queueSizeNoneButton.hover": "VYPNUTO", - "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", - "vcmi.battleOptions.queueSizeSmallButton.hover": "MALÁ", - "vcmi.battleOptions.queueSizeBigButton.hover": "VELKÁ", - "vcmi.battleOptions.queueSizeNoneButton.help": "Nezobrazovat frontu pořadí tahů.", - "vcmi.battleOptions.queueSizeAutoButton.help": "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)", - "vcmi.battleOptions.queueSizeSmallButton.help": "Zobrazit MALOU frontu pořadí tahů.", - "vcmi.battleOptions.queueSizeBigButton.help": "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).", - "vcmi.battleOptions.animationsSpeed1.hover": "", - "vcmi.battleOptions.animationsSpeed5.hover": "", - "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Nastavit rychlost animací na velmi pomalé.", - "vcmi.battleOptions.animationsSpeed5.help": "Nastavit rychlost animací na velmi rychlé.", - "vcmi.battleOptions.animationsSpeed6.help": "Nastavit rychlost animací na okamžité.", - "vcmi.battleOptions.movementHighlightOnHover.hover": "Zvýraznění pohybu při najetí", - "vcmi.battleOptions.movementHighlightOnHover.help": "{Zvýraznění pohybu při najetí}\n\nZvýraznit rozsah pohybu jednotky při najetí na něj.", - "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Zobrazit omezení dostřelu střelců", - "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Zobrazit omezení dostřelu střelců při najetí}\n\nZobrazit dostřel střelce při najetí na něj.", - "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Zobrazit okno statistik hrdinů", - "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Zobrazit okno statistik hrdinů}\n\nTrvale zapne okno statistiky hrdinů, které ukazuje hlavní schopnosti a magickou energii.", - "vcmi.battleOptions.skipBattleIntroMusic.hover": "Přeskočit úvodní hudbu", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Přeskočit úvodní hudbu}\n\nPovolí akce při úvodní hudbě přehrávané při začátku každé bitvy.", - "vcmi.battleOptions.endWithAutocombat.hover": "Přeskočit bitvu", - "vcmi.battleOptions.endWithAutocombat.help": "{Přeskočit bitvu}\n\nAutomatický boj okamžitě dohraje bitvu do konce.", - "vcmi.battleOptions.showQuickSpell.hover": "Zobrazit rychlý panel kouzel", - "vcmi.battleOptions.showQuickSpell.help": "{Zobrazit rychlý panel kouzel}\n\nZobrazí panel pro rychlý výběr kouzel.", + "vcmi.battleOptions.queueSizeLabel.hover" : "Zobrazit frontu pořadí tahů", + "vcmi.battleOptions.queueSizeNoneButton.hover" : "VYPNUTO", + "vcmi.battleOptions.queueSizeAutoButton.hover" : "AUTO", + "vcmi.battleOptions.queueSizeSmallButton.hover" : "MALÁ", + "vcmi.battleOptions.queueSizeBigButton.hover" : "VELKÁ", + "vcmi.battleOptions.queueSizeNoneButton.help" : "Nezobrazovat frontu pořadí tahů.", + "vcmi.battleOptions.queueSizeAutoButton.help" : "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)", + "vcmi.battleOptions.queueSizeSmallButton.help" : "Zobrazit MALOU frontu pořadí tahů.", + "vcmi.battleOptions.queueSizeBigButton.help" : "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).", + "vcmi.battleOptions.animationsSpeed1.hover" : "", + "vcmi.battleOptions.animationsSpeed5.hover" : "", + "vcmi.battleOptions.animationsSpeed6.hover" : "", + "vcmi.battleOptions.animationsSpeed1.help" : "Nastavit rychlost animací na velmi pomalé.", + "vcmi.battleOptions.animationsSpeed5.help" : "Nastavit rychlost animací na velmi rychlé.", + "vcmi.battleOptions.animationsSpeed6.help" : "Nastavit rychlost animací na okamžité.", + "vcmi.battleOptions.movementHighlightOnHover.hover" : "Zvýraznění pohybu při najetí", + "vcmi.battleOptions.movementHighlightOnHover.help" : "{Zvýraznění pohybu při najetí}\n\nZvýraznit rozsah pohybu jednotky při najetí na něj.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover" : "Zobrazit omezení dostřelu střelců", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help" : "{Zobrazit omezení dostřelu střelců při najetí}\n\nZobrazit dostřel střelce při najetí na něj.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover" : "Zobrazit okno statistik hrdinů", + "vcmi.battleOptions.showStickyHeroInfoWindows.help" : "{Zobrazit okno statistik hrdinů}\n\nTrvale zapne okno statistiky hrdinů, které ukazuje hlavní schopnosti a magickou energii.", + "vcmi.battleOptions.skipBattleIntroMusic.hover" : "Přeskočit úvodní hudbu", + "vcmi.battleOptions.skipBattleIntroMusic.help" : "{Přeskočit úvodní hudbu}\n\nPovolí akce při úvodní hudbě přehrávané při začátku každé bitvy.", + "vcmi.battleOptions.endWithAutocombat.hover" : "Přeskočit bitvu", + "vcmi.battleOptions.endWithAutocombat.help" : "{Přeskočit bitvu}\n\nAutomatický boj okamžitě dohraje bitvu do konce.", + "vcmi.battleOptions.showQuickSpell.hover" : "Zobrazit rychlý panel kouzel", + "vcmi.battleOptions.showQuickSpell.help" : "{Zobrazit rychlý panel kouzel}\n\nZobrazí panel pro rychlý výběr kouzel.", "vcmi.adventureMap.revisitObject.hover" : "Znovu navštívit objekt", "vcmi.adventureMap.revisitObject.help" : "{Znovu navštívit objekt}\n\nPokud hrdina právě stojí na objektu na mapě, může toto místo znovu navštívit.", @@ -365,8 +404,8 @@ "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Zobrazit dostupné jednotky}\n\nZobrazit počet jednotek dostupných ke koupení místo jejich týdenního přírůstku v přehledu města. (levý spodní okraj obrazovky města).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Zobrazit týdenní přírůstek jednotek", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Zobrazit týdenní přírůstek jednotek}\n\nZobrazit týdenní přírůstek jednotek místo dostupného počtu ke koupení v přehledu města (levý spodní okraj obrazovky města).", - "vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompaktní informace o jednotkách", - "vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompaktní informace o jednotkách}\n\nZobrazit menší informace o jednotkách města v jeho přehledu (levý spodní okraj obrazovky města).", + "vcmi.otherOptions.compactTownCreatureInfo.hover" : "Kompaktní informace o jednotkách", + "vcmi.otherOptions.compactTownCreatureInfo.help" : "{Kompaktní informace o jednotkách}\n\nZobrazit menší informace o jednotkách města v jeho přehledu (levý spodní okraj obrazovky města).", "vcmi.townHall.missingBase" : "Nejdříve musí být postavena základní budova %s", "vcmi.townHall.noCreaturesToRecruit" : "Nejsou k dispozici žádné jednotky k najmutí!", @@ -568,158 +607,182 @@ "mapObject.core.hillFort.object.description" : "Zde můžeš vylepšit jednotky. Vylepšení jednotek úrovně 1 až 4 je zde levnější než v jejich domovském městě.", - "core.bonus.ADDITIONAL_ATTACK.name": "Dvojitý útok", - "core.bonus.ADDITIONAL_ATTACK.description": "Útočí dvakrát", - "core.bonus.ADDITIONAL_RETALIATION.name": "Další odvetné útoky", - "core.bonus.ADDITIONAL_RETALIATION.description": "Může odvetně zaútočit ${val} krát navíc", - "core.bonus.AIR_IMMUNITY.name": "Odolnost vůči vzdušné magii", - "core.bonus.AIR_IMMUNITY.description": "Imunní vůči všem kouzlům školy vzdušné magie", - "core.bonus.ATTACKS_ALL_ADJACENT.name": "Útok na všechny kolem", - "core.bonus.ATTACKS_ALL_ADJACENT.description": "Útočí na všechny sousední nepřátele", - "core.bonus.BLOCKS_RETALIATION.name": "Žádná odveta", - "core.bonus.BLOCKS_RETALIATION.description": "Nepřítel nemůže odvetně zaútočit", - "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Žádná střelecká odveta", - "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Nepřítel nemůže odvetně zaútočit střeleckým útokem", - "core.bonus.CATAPULT.name": "Katapult", - "core.bonus.CATAPULT.description": "Útočí na ochranné hradby", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Snížit cenu kouzel (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Snižuje náklady na kouzla pro hrdinu o ${val}", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tlumič magie (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Zvyšuje náklady na kouzla nepřítele o ${val}", - "core.bonus.CHARGE_IMMUNITY.name": "Odolnost vůči Nájezdu", - "core.bonus.CHARGE_IMMUNITY.description": "Imunní vůči Nájezdu Jezdců a Šampionů", - "core.bonus.DARKNESS.name": "Závoj temnoty", - "core.bonus.DARKNESS.description": "Vytváří závoj temnoty s poloměrem ${val}", - "core.bonus.DEATH_STARE.name": "Smrtící pohled (${val}%)", - "core.bonus.DEATH_STARE.description": "Má ${val}% šanci zabít jednu jednotku", - "core.bonus.DEFENSIVE_STANCE.name": "Obranný bonus", - "core.bonus.DEFENSIVE_STANCE.description": "+${val} k obraně při bránění", - "core.bonus.DESTRUCTION.name": "Zničení", - "core.bonus.DESTRUCTION.description": "Má ${val}% šanci zabít další jednotky po útoku", - "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Smrtelný úder", - "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Má ${val}% šanci způsobit dvojnásobné základní poškození při útoku", - "core.bonus.DRAGON_NATURE.name": "Dračí povaha", - "core.bonus.DRAGON_NATURE.description": "Jednotka má Dračí povahu", - "core.bonus.EARTH_IMMUNITY.name": "Odolnost vůči zemské magii", - "core.bonus.EARTH_IMMUNITY.description": "Imunní vůči všem kouzlům školy zemské magie", - "core.bonus.ENCHANTER.name": "Zaklínač", - "core.bonus.ENCHANTER.description": "Může každé kolo sesílat masové kouzlo ${subtype.spell}", - "core.bonus.ENCHANTED.name": "Očarovaný", - "core.bonus.ENCHANTED.description": "Je pod trvalým účinkem kouzla ${subtype.spell}", - "core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignorování útoku (${val}%)", - "core.bonus.ENEMY_ATTACK_REDUCTION.description": "Při útoku je ignorováno ${val}% útočníkovy síly", - "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignorování obrany (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Pří útoku nebude bráno v potaz ${val}% bodů obrany obránce", - "core.bonus.FIRE_IMMUNITY.name": "Odolnost vůči ohnivé magii", - "core.bonus.FIRE_IMMUNITY.description": "Imunní vůči všem kouzlům školy ohnivé magie", - "core.bonus.FIRE_SHIELD.name": "Ohnivý štít (${val}%)", - "core.bonus.FIRE_SHIELD.description": "Odrazí část zranění při útoku z blízka", - "core.bonus.FIRST_STRIKE.name": "První úder", - "core.bonus.FIRST_STRIKE.description": "Tato jednotka útočí dříve, než je napadena", - "core.bonus.FEAR.name": "Strach", - "core.bonus.FEAR.description": "Vyvolává strach u nepřátelské jednotky", - "core.bonus.FEARLESS.name": "Nebojácnost", - "core.bonus.FEARLESS.description": "Imunní vůči schopnosti Strach", - "core.bonus.FEROCITY.name": "Zuřivost", - "core.bonus.FEROCITY.description": "Útočí ${val} krát navíc, pokud někoho zabije", - "core.bonus.FLYING.name": "Létání", - "core.bonus.FLYING.description": "Při pohybu létá (ignoruje překážky)", - "core.bonus.FREE_SHOOTING.name": "Střelba zblízka", - "core.bonus.FREE_SHOOTING.description": "Může použít výstřely i při útoku zblízka", - "core.bonus.GARGOYLE.name": "Chrlič", - "core.bonus.GARGOYLE.description": "Nemůže být oživen ani vyléčen", - "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Snižuje poškození (${val}%)", - "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Snižuje poškození od útoků z dálky a blízka", - "core.bonus.HATE.name": "Nenávidí ${subtype.creature}", - "core.bonus.HATE.description": "Způsobuje ${val}% více poškození vůči ${subtype.creature}", - "core.bonus.HEALER.name": "Léčitel", - "core.bonus.HEALER.description": "Léčí spojenecké jednotky", - "core.bonus.HP_REGENERATION.name": "Regenerace", - "core.bonus.HP_REGENERATION.description": "Každé kolo regeneruje ${val} bodů zdraví", - "core.bonus.JOUSTING.name": "Nájezd šampionů", - "core.bonus.JOUSTING.description": "+${val}% poškození za každé projité pole", - "core.bonus.KING.name": "Král", - "core.bonus.KING.description": "Zranitelný proti zabijákovi úrovně ${val} a vyšší", - "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odolnost kouzel 1-${val}", - "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odolnost vůči kouzlům úrovní 1-${val}", - "core.bonus.LIMITED_SHOOTING_RANGE.name": "Omezený dostřel", - "core.bonus.LIMITED_SHOOTING_RANGE.description": "Není schopen zasáhnout jednotky vzdálenější než ${val} polí", - "core.bonus.LIFE_DRAIN.name": "Vysávání života (${val}%)", - "core.bonus.LIFE_DRAIN.description": "Vysává ${val}% způsobeného poškození", - "core.bonus.MANA_CHANNELING.name": "Kanál magie ${val}%", - "core.bonus.MANA_CHANNELING.description": "Poskytuje vašemu hrdinovi ${val}% many použité nepřítelem", - "core.bonus.MANA_DRAIN.name": "Vysávání many", - "core.bonus.MANA_DRAIN.description": "Vysává ${val} many každý tah", - "core.bonus.MAGIC_MIRROR.name": "Magické zrcadlo (${val}%)", - "core.bonus.MAGIC_MIRROR.description": "Má ${val}% šanci odrazit útočné kouzlo na nepřátelskou jednotku", - "core.bonus.MAGIC_RESISTANCE.name": "Magická odolnost (${val}%)", - "core.bonus.MAGIC_RESISTANCE.description": "Má ${val}% šanci odolat nepřátelskému kouzlu", - "core.bonus.MIND_IMMUNITY.name": "Imunita vůči kouzlům mysli", - "core.bonus.MIND_IMMUNITY.description": "Imunní vůči kouzlům mysli", - "core.bonus.NO_DISTANCE_PENALTY.name": "Žádná penalizace vzdálenosti", - "core.bonus.NO_DISTANCE_PENALTY.description": "Způsobuje plné poškození na jakoukoliv vzdálenost", - "core.bonus.NO_MELEE_PENALTY.name": "Bez penalizace útoku zblízka", - "core.bonus.NO_MELEE_PENALTY.description": "Jednotka není penalizována za útok zblízka", - "core.bonus.NO_MORALE.name": "Neutrální morálka", - "core.bonus.NO_MORALE.description": "Jednotka je imunní vůči efektům morálky", - "core.bonus.NO_WALL_PENALTY.name": "Bez penalizace hradbami", - "core.bonus.NO_WALL_PENALTY.description": "Plné poškození během obléhání", - "core.bonus.NON_LIVING.name": "Neživý", - "core.bonus.NON_LIVING.description": "Imunní vůči mnohým efektům", - "core.bonus.RANDOM_SPELLCASTER.name": "Náhodný kouzelník", - "core.bonus.RANDOM_SPELLCASTER.description": "Může seslat náhodné kouzlo", - "core.bonus.RANGED_RETALIATION.name": "Střelecká odveta", - "core.bonus.RANGED_RETALIATION.description": "Může provést protiútok na dálku", - "core.bonus.RECEPTIVE.name": "Vnímavý", - "core.bonus.RECEPTIVE.description": "Nemá imunitu na přátelská kouzla", - "core.bonus.REBIRTH.name": "Znovuzrození (${val}%)", - "core.bonus.REBIRTH.description": "${val}% jednotek povstane po smrti", - "core.bonus.RETURN_AFTER_STRIKE.name": "Útok a návrat", - "core.bonus.RETURN_AFTER_STRIKE.description": "Navrátí se po útoku na zblízka", - "core.bonus.REVENGE.name": "Pomsta", - "core.bonus.REVENGE.description": "Způsobuje extra poškození na základě ztrát útočníka v bitvě", - "core.bonus.SHOOTER.name": "Střelec", - "core.bonus.SHOOTER.description": "Jednotka může střílet", - "core.bonus.SHOOTS_ALL_ADJACENT.name": "Střílí všude kolem", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "Střelecký útok této jednotky zasáhne všechny cíle v malé oblasti", - "core.bonus.SOUL_STEAL.name": "Zloděj duší", - "core.bonus.SOUL_STEAL.description": "Získává ${val} nové jednotky za každého zabitého nepřítele", - "core.bonus.SPELLCASTER.name": "Kouzelník", - "core.bonus.SPELLCASTER.description": "Může seslat kouzlo ${subtype.spell}", - "core.bonus.SPELL_AFTER_ATTACK.name": "Sesílá po útoku", - "core.bonus.SPELL_AFTER_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} po útoku", - "core.bonus.SPELL_BEFORE_ATTACK.name": "Sesílá před útokem", - "core.bonus.SPELL_BEFORE_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} před útokem", - "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Magická odolnost", - "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Poškození kouzly sníženo o ${val}%.", - "core.bonus.SPELL_IMMUNITY.name": "Imunita vůči kouzlům", - "core.bonus.SPELL_IMMUNITY.description": "Imunní vůči ${subtype.spell}", - "core.bonus.SPELL_LIKE_ATTACK.name": "Útok kouzlem", - "core.bonus.SPELL_LIKE_ATTACK.description": "Útočí kouzlem ${subtype.spell}", - "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura odporu", - "core.bonus.SPELL_RESISTANCE_AURA.description": "Jednotky poblíž získají ${val}% magickou odolnost", - "core.bonus.SUMMON_GUARDIANS.name": "Přivolání ochránců", - "core.bonus.SUMMON_GUARDIANS.description": "Na začátku bitvy přivolá ${subtype.creature} (${val}%)", - "core.bonus.SYNERGY_TARGET.name": "Synergizovatelný", - "core.bonus.SYNERGY_TARGET.description": "Tato jednotka je náchylná k synergickým efektům", - "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Dech", - "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Útok dechem (dosah 2 polí)", - "core.bonus.THREE_HEADED_ATTACK.name": "Tříhlavý útok", - "core.bonus.THREE_HEADED_ATTACK.description": "Útočí na tři sousední jednotky", - "core.bonus.TRANSMUTATION.name": "Transmutace", - "core.bonus.TRANSMUTATION.description": "${val}% šance na přeměnu napadené jednotky na jiný typ", - "core.bonus.UNDEAD.name": "Nemrtvý", - "core.bonus.UNDEAD.description": "Jednotka je nemrtvá", - "core.bonus.UNLIMITED_RETALIATIONS.name": "Neomezené odvetné útoky", - "core.bonus.UNLIMITED_RETALIATIONS.description": "Může provést neomezený počet odvetných útoků", - "core.bonus.WATER_IMMUNITY.name": "Odolnost vůči vodní magii", - "core.bonus.WATER_IMMUNITY.description": "Imunní vůči všem kouzlům školy vodní magie", - "core.bonus.WIDE_BREATH.name": "Široký dech", - "core.bonus.WIDE_BREATH.description": "Široký útok dechem (více polí)", - "core.bonus.DISINTEGRATE.name": "Rozpad", - "core.bonus.DISINTEGRATE.description": "Po smrti nezůstane žádné tělo", - "core.bonus.INVINCIBLE.name": "Neporazitelný", - "core.bonus.INVINCIBLE.description": "Nelze ovlivnit žádným efektem", - "core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Trojitý dech", - "core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Útok trojitým dechem (útok přes 3 směry)" + "core.bonus.ADDITIONAL_ATTACK.name" : "Dvojitý útok", + "core.bonus.ADDITIONAL_ATTACK.description" : "Útočí dvakrát", + "core.bonus.ADDITIONAL_RETALIATION.name" : "Další odvetné útoky", + "core.bonus.ADDITIONAL_RETALIATION.description" : "Může odvetně zaútočit ${val} krát navíc", + "core.bonus.AIR_IMMUNITY.name" : "Odolnost vůči vzdušné magii", + "core.bonus.AIR_IMMUNITY.description" : "Imunní vůči všem kouzlům školy vzdušné magie", + "core.bonus.ATTACKS_ALL_ADJACENT.name" : "Útok na všechny kolem", + "core.bonus.ATTACKS_ALL_ADJACENT.description" : "Útočí na všechny sousední nepřátele", + "core.bonus.BLOCKS_RETALIATION.name" : "Žádná odveta", + "core.bonus.BLOCKS_RETALIATION.description" : "Nepřítel nemůže odvetně zaútočit", + "core.bonus.BLOCKS_RANGED_RETALIATION.name" : "Žádná střelecká odveta", + "core.bonus.BLOCKS_RANGED_RETALIATION.description" : "Nepřítel nemůže odvetně zaútočit střeleckým útokem", + "core.bonus.CATAPULT.name" : "Katapult", + "core.bonus.CATAPULT.description" : "Útočí na ochranné hradby", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name" : "Snížit cenu kouzel (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Snižuje náklady na kouzla pro hrdinu o ${val}", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name" : "Tlumič magie (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description" : "Zvyšuje náklady na kouzla nepřítele o ${val}", + "core.bonus.CHARGE_IMMUNITY.name" : "Odolnost vůči Nájezdu", + "core.bonus.CHARGE_IMMUNITY.description" : "Imunní vůči Nájezdu Jezdců a Šampionů", + "core.bonus.DARKNESS.name" : "Závoj temnoty", + "core.bonus.DARKNESS.description" : "Vytváří závoj temnoty s poloměrem ${val}", + "core.bonus.DEATH_STARE.name" : "Smrtící pohled (${val}%)", + "core.bonus.DEATH_STARE.description" : "Má ${val}% šanci zabít jednu jednotku", + "core.bonus.DEFENSIVE_STANCE.name" : "Obranný bonus", + "core.bonus.DEFENSIVE_STANCE.description" : "+${val} k obraně při bránění", + "core.bonus.DESTRUCTION.name" : "Zničení", + "core.bonus.DESTRUCTION.description" : "Má ${val}% šanci zabít další jednotky po útoku", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name" : "Smrtelný úder", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description" : "Má ${val}% šanci způsobit dvojnásobné základní poškození při útoku", + "core.bonus.DRAGON_NATURE.name" : "Dračí povaha", + "core.bonus.DRAGON_NATURE.description" : "Jednotka má Dračí povahu", + "core.bonus.EARTH_IMMUNITY.name" : "Odolnost vůči zemské magii", + "core.bonus.EARTH_IMMUNITY.description" : "Imunní vůči všem kouzlům školy zemské magie", + "core.bonus.ENCHANTER.name" : "Zaklínač", + "core.bonus.ENCHANTER.description" : "Může každé kolo sesílat masové kouzlo ${subtype.spell}", + "core.bonus.ENCHANTED.name" : "Očarovaný", + "core.bonus.ENCHANTED.description" : "Je pod trvalým účinkem kouzla ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ignorování útoku (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description" : "Při útoku je ignorováno ${val}% útočníkovy síly", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name" : "Ignorování obrany (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "Pří útoku nebude bráno v potaz ${val}% bodů obrany obránce", + "core.bonus.FIRE_IMMUNITY.name" : "Odolnost vůči ohnivé magii", + "core.bonus.FIRE_IMMUNITY.description" : "Imunní vůči všem kouzlům školy ohnivé magie", + "core.bonus.FIRE_SHIELD.name" : "Ohnivý štít (${val}%)", + "core.bonus.FIRE_SHIELD.description" : "Odrazí část zranění při útoku z blízka", + "core.bonus.FIRST_STRIKE.name" : "První úder", + "core.bonus.FIRST_STRIKE.description" : "Tato jednotka útočí dříve, než je napadena", + "core.bonus.FEAR.name" : "Strach", + "core.bonus.FEAR.description" : "Vyvolává strach u nepřátelské jednotky", + "core.bonus.FEARLESS.name" : "Nebojácnost", + "core.bonus.FEARLESS.description" : "Imunní vůči schopnosti Strach", + "core.bonus.FEROCITY.name" : "Zuřivost", + "core.bonus.FEROCITY.description" : "Útočí ${val} krát navíc, pokud někoho zabije", + "core.bonus.FLYING.name" : "Létání", + "core.bonus.FLYING.description" : "Při pohybu létá (ignoruje překážky)", + "core.bonus.FREE_SHOOTING.name" : "Střelba zblízka", + "core.bonus.FREE_SHOOTING.description" : "Může použít výstřely i při útoku zblízka", + "core.bonus.GARGOYLE.name" : "Chrlič", + "core.bonus.GARGOYLE.description" : "Nemůže být oživen ani vyléčen", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Snižuje poškození (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description" : "Snižuje poškození od útoků z dálky a blízka", + "core.bonus.HATE.name" : "Nenávidí ${subtype.creature}", + "core.bonus.HATE.description" : "Způsobuje ${val}% více poškození vůči ${subtype.creature}", + "core.bonus.HEALER.name" : "Léčitel", + "core.bonus.HEALER.description" : "Léčí spojenecké jednotky", + "core.bonus.HP_REGENERATION.name" : "Regenerace", + "core.bonus.HP_REGENERATION.description" : "Každé kolo regeneruje ${val} bodů zdraví", + "core.bonus.JOUSTING.name" : "Nájezd šampionů", + "core.bonus.JOUSTING.description" : "+${val}% poškození za každé projité pole", + "core.bonus.KING.name" : "Král", + "core.bonus.KING.description" : "Zranitelný proti zabijákovi úrovně ${val} a vyšší", + "core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Odolnost kouzel 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Odolnost vůči kouzlům úrovní 1-${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Omezený dostřel", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Není schopen zasáhnout jednotky vzdálenější než ${val} polí", + "core.bonus.LIFE_DRAIN.name" : "Vysávání života (${val}%)", + "core.bonus.LIFE_DRAIN.description" : "Vysává ${val}% způsobeného poškození", + "core.bonus.MANA_CHANNELING.name" : "Kanál magie ${val}%", + "core.bonus.MANA_CHANNELING.description" : "Poskytuje vašemu hrdinovi ${val}% many použité nepřítelem", + "core.bonus.MANA_DRAIN.name" : "Vysávání many", + "core.bonus.MANA_DRAIN.description" : "Vysává ${val} many každý tah", + "core.bonus.MAGIC_MIRROR.name" : "Magické zrcadlo (${val}%)", + "core.bonus.MAGIC_MIRROR.description" : "Má ${val}% šanci odrazit útočné kouzlo na nepřátelskou jednotku", + "core.bonus.MAGIC_RESISTANCE.name" : "Magická odolnost (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description" : "Má ${val}% šanci odolat nepřátelskému kouzlu", + "core.bonus.MIND_IMMUNITY.name" : "Imunita vůči kouzlům mysli", + "core.bonus.MIND_IMMUNITY.description" : "Imunní vůči kouzlům mysli", + "core.bonus.NO_DISTANCE_PENALTY.name" : "Žádná penalizace vzdálenosti", + "core.bonus.NO_DISTANCE_PENALTY.description" : "Způsobuje plné poškození na jakoukoliv vzdálenost", + "core.bonus.NO_MELEE_PENALTY.name" : "Bez penalizace útoku zblízka", + "core.bonus.NO_MELEE_PENALTY.description" : "Jednotka není penalizována za útok zblízka", + "core.bonus.NO_MORALE.name" : "Neutrální morálka", + "core.bonus.NO_MORALE.description" : "Jednotka je imunní vůči efektům morálky", + "core.bonus.NO_WALL_PENALTY.name" : "Bez penalizace hradbami", + "core.bonus.NO_WALL_PENALTY.description" : "Plné poškození během obléhání", + "core.bonus.NON_LIVING.name" : "Neživý", + "core.bonus.NON_LIVING.description" : "Imunní vůči mnohým efektům", + "core.bonus.RANDOM_SPELLCASTER.name" : "Náhodný kouzelník", + "core.bonus.RANDOM_SPELLCASTER.description" : "Může seslat náhodné kouzlo", + "core.bonus.RANGED_RETALIATION.name" : "Střelecká odveta", + "core.bonus.RANGED_RETALIATION.description" : "Může provést protiútok na dálku", + "core.bonus.RECEPTIVE.name" : "Vnímavý", + "core.bonus.RECEPTIVE.description" : "Nemá imunitu na přátelská kouzla", + "core.bonus.REBIRTH.name" : "Znovuzrození (${val}%)", + "core.bonus.REBIRTH.description" : "${val}% jednotek povstane po smrti", + "core.bonus.RETURN_AFTER_STRIKE.name" : "Útok a návrat", + "core.bonus.RETURN_AFTER_STRIKE.description" : "Navrátí se po útoku na zblízka", + "core.bonus.REVENGE.name" : "Pomsta", + "core.bonus.REVENGE.description" : "Způsobuje extra poškození na základě ztrát útočníka v bitvě", + "core.bonus.SHOOTER.name" : "Střelec", + "core.bonus.SHOOTER.description" : "Jednotka může střílet", + "core.bonus.SHOOTS_ALL_ADJACENT.name" : "Střílí všude kolem", + "core.bonus.SHOOTS_ALL_ADJACENT.description" : "Střelecký útok této jednotky zasáhne všechny cíle v malé oblasti", + "core.bonus.SOUL_STEAL.name" : "Zloděj duší", + "core.bonus.SOUL_STEAL.description" : "Získává ${val} nové jednotky za každého zabitého nepřítele", + "core.bonus.SPELLCASTER.name" : "Kouzelník", + "core.bonus.SPELLCASTER.description" : "Může seslat kouzlo ${subtype.spell}", + "core.bonus.SPELL_AFTER_ATTACK.name" : "Sesílá po útoku", + "core.bonus.SPELL_AFTER_ATTACK.description" : "Má ${val}% šanci seslat ${subtype.spell} po útoku", + "core.bonus.SPELL_BEFORE_ATTACK.name" : "Sesílá před útokem", + "core.bonus.SPELL_BEFORE_ATTACK.description" : "Má ${val}% šanci seslat ${subtype.spell} před útokem", + "core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Magická odolnost", + "core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Poškození kouzly sníženo o ${val}%.", + "core.bonus.SPELL_IMMUNITY.name" : "Imunita vůči kouzlům", + "core.bonus.SPELL_IMMUNITY.description" : "Imunní vůči ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name" : "Útok kouzlem", + "core.bonus.SPELL_LIKE_ATTACK.description" : "Útočí kouzlem ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name" : "Aura odporu", + "core.bonus.SPELL_RESISTANCE_AURA.description" : "Jednotky poblíž získají ${val}% magickou odolnost", + "core.bonus.SUMMON_GUARDIANS.name" : "Přivolání ochránců", + "core.bonus.SUMMON_GUARDIANS.description" : "Na začátku bitvy přivolá ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name" : "Synergizovatelný", + "core.bonus.SYNERGY_TARGET.description" : "Tato jednotka je náchylná k synergickým efektům", + "core.bonus.TWO_HEX_ATTACK_BREATH.name" : "Dech", + "core.bonus.TWO_HEX_ATTACK_BREATH.description" : "Útok dechem (dosah 2 polí)", + "core.bonus.THREE_HEADED_ATTACK.name" : "Tříhlavý útok", + "core.bonus.THREE_HEADED_ATTACK.description" : "Útočí na tři sousední jednotky", + "core.bonus.TRANSMUTATION.name" : "Transmutace", + "core.bonus.TRANSMUTATION.description" : "${val}% šance na přeměnu napadené jednotky na jiný typ", + "core.bonus.UNDEAD.name" : "Nemrtvý", + "core.bonus.UNDEAD.description" : "Jednotka je nemrtvá", + "core.bonus.UNLIMITED_RETALIATIONS.name" : "Neomezené odvetné útoky", + "core.bonus.UNLIMITED_RETALIATIONS.description" : "Může provést neomezený počet odvetných útoků", + "core.bonus.WATER_IMMUNITY.name" : "Odolnost vůči vodní magii", + "core.bonus.WATER_IMMUNITY.description" : "Imunní vůči všem kouzlům školy vodní magie", + "core.bonus.WIDE_BREATH.name" : "Široký dech", + "core.bonus.WIDE_BREATH.description" : "Široký útok dechem (více polí)", + "core.bonus.DISINTEGRATE.name" : "Rozpad", + "core.bonus.DISINTEGRATE.description" : "Po smrti nezůstane žádné tělo", + "core.bonus.INVINCIBLE.name" : "Neporazitelný", + "core.bonus.INVINCIBLE.description" : "Nelze ovlivnit žádným efektem", + "core.bonus.MECHANICAL.description" : "Imunita vůči mnoha efektům, opravitelné", + "core.bonus.MECHANICAL.name" : "Mechanický", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Trojitý dech", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Útok trojitým dechem (útok přes 3 směry)", + + "spell.core.castleMoat.name" : "Hradní příkop", + "spell.core.castleMoatTrigger.name" : "Hradní příkop", + "spell.core.catapultShot.name" : "Výstřel z katapultu", + "spell.core.cyclopsShot.name" : "Obléhací střela", + "spell.core.dungeonMoat.name" : "Vařící olej", + "spell.core.dungeonMoatTrigger.name" : "Vařící olej", + "spell.core.fireWallTrigger.name" : "Ohnivá zeď", + "spell.core.firstAid.name" : "První pomoc", + "spell.core.fortressMoat.name" : "Vařící dehet", + "spell.core.fortressMoatTrigger.name" : "Vařící dehet", + "spell.core.infernoMoat.name" : "Láva", + "spell.core.infernoMoatTrigger.name" : "Láva", + "spell.core.landMineTrigger.name" : "Pozemní mina", + "spell.core.necropolisMoat.name" : "Hřbitov", + "spell.core.necropolisMoatTrigger.name" : "Hřbitov", + "spell.core.rampartMoat.name" : "Ostružiní", + "spell.core.rampartMoatTrigger.name" : "Ostružiní", + "spell.core.strongholdMoat.name" : "Dřevěné bodce", + "spell.core.strongholdMoatTrigger.name" : "Dřevěné bodce", + "spell.core.summonDemons.name" : "Přivolání démonů", + "spell.core.towerMoat.name" : "Pozemní mina" } \ No newline at end of file diff --git a/Mods/vcmi/config/english.json b/Mods/vcmi/Content/config/english.json similarity index 93% rename from Mods/vcmi/config/english.json rename to Mods/vcmi/Content/config/english.json index 3eb1c288f..d5a7af9dd 100644 --- a/Mods/vcmi/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -120,6 +120,44 @@ "vcmi.lobby.deleteFile" : "Do you want to delete following file?", "vcmi.lobby.deleteFolder" : "Do you want to delete following folder?", "vcmi.lobby.deleteMode" : "Switch to delete mode and back", + + "vcmi.broadcast.failedLoadGame" : "Failed to load game", + "vcmi.broadcast.command" : "Use '!help' to list available commands", + "vcmi.broadcast.simturn.end" : "Simultaneous turns have ended", + "vcmi.broadcast.simturn.endBetween" : "Simultaneous turns between players %s and %s have ended", + "vcmi.broadcast.serverProblem" : "Server encountered a problem", + "vcmi.broadcast.gameTerminated" : "game was terminated", + "vcmi.broadcast.gameSavedAs" : "game saved as", + "vcmi.broadcast.noCheater" : "No cheaters registered!", + "vcmi.broadcast.playerCheater" : "Player %s is cheater!", + "vcmi.broadcast.statisticFile" : "Statistic files can be found in %s directory", + "vcmi.broadcast.help.commands" : "Available commands to host:", + "vcmi.broadcast.help.exit" : "'!exit' - immediately ends current game", + "vcmi.broadcast.help.kick" : "'!kick ' - kick specified player from the game", + "vcmi.broadcast.help.save" : "'!save ' - save game under specified filename", + "vcmi.broadcast.help.statistic" : "'!statistic' - save game statistics as csv file", + "vcmi.broadcast.help.commandsAll" : "Available commands to all players:", + "vcmi.broadcast.help.help" : "'!help' - display this help", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - list players that entered cheat command during game", + "vcmi.broadcast.help.vote" : "'!vote' - allows to change some game settings if all players vote for it", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - allow simultaneous turns for specified number of days, or until contact", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - force simultaneous turns for specified number of days, blocking player contacts", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - abort simultaneous turns once this turn ends", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolong base timer for all players by specified number of seconds", + "vcmi.broadcast.vote.noActive" : "No active voting!", + "vcmi.broadcast.vote.yes" : "yes", + "vcmi.broadcast.vote.no" : "no", + "vcmi.broadcast.vote.notRecognized" : "Voting command not recognized!", + "vcmi.broadcast.vote.success.untilContacts" : "Voting successful. Simultaneous turns will run for %s more days, or until contact", + "vcmi.broadcast.vote.success.contactsBlocked" : "Voting successful. Simultaneous turns will run for %s more days. Contacts are blocked", + "vcmi.broadcast.vote.success.nextDay" : "Voting successful. Simultaneous turns will end on next day", + "vcmi.broadcast.vote.success.timer" : "Voting successful. Timer for all players has been prolonger for %s seconds", + "vcmi.broadcast.vote.aborted" : "Player voted against change. Voting aborted", + "vcmi.broadcast.vote.start.untilContacts" : "Started voting to allow simultaneous turns for %s more days", + "vcmi.broadcast.vote.start.contactsBlocked" : "Started voting to force simultaneous turns for %s more days", + "vcmi.broadcast.vote.start.nextDay" : "Started voting to end simultaneous turns starting from next day", + "vcmi.broadcast.vote.start.timer" : "Started voting to prolong timer for all players by %s seconds", + "vcmi.broadcast.vote.hint" : "Type '!vote yes' to agree to this change or '!vote no' to vote against it", "vcmi.lobby.login.title" : "VCMI Online Lobby", "vcmi.lobby.login.username" : "Username:", @@ -128,6 +166,7 @@ "vcmi.lobby.login.create" : "New Account", "vcmi.lobby.login.login" : "Login", "vcmi.lobby.login.as" : "Login as %s", + "vcmi.lobby.login.spectator" : "Spectator", "vcmi.lobby.header.rooms" : "Game Rooms - %d", "vcmi.lobby.header.channels" : "Chat Channels", "vcmi.lobby.header.chat.global" : "Global Game Chat - %s", // %s -> language name @@ -188,10 +227,9 @@ "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", "vcmi.server.errors.modsToEnable" : "{Following mods are required}", "vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}", - "vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n", - "vcmi.server.errors.modDependencyLoop" : "Failed to load mod {'%s'}!\n It maybe in a (soft) dependency loop.", - "vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!", + "vcmi.server.errors.wrongIdentified" : "You were identified as player %s while expecting %s", + "vcmi.server.errors.notAllowed" : "You are not allowed to perform this action!", "vcmi.dimensionDoor.seaToLandError" : "It's not possible to teleport from sea to land or vice versa with a Dimension Door.", diff --git a/Mods/vcmi/config/french.json b/Mods/vcmi/Content/config/french.json similarity index 100% rename from Mods/vcmi/config/french.json rename to Mods/vcmi/Content/config/french.json diff --git a/Mods/vcmi/config/german.json b/Mods/vcmi/Content/config/german.json similarity index 99% rename from Mods/vcmi/config/german.json rename to Mods/vcmi/Content/config/german.json index cb7e315d0..48c974d92 100644 --- a/Mods/vcmi/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -120,6 +120,8 @@ "vcmi.lobby.deleteFile" : "Möchtet Ihr folgende Datei löschen?", "vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?", "vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück", + + "vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten", "vcmi.lobby.login.title" : "VCMI Online Lobby", "vcmi.lobby.login.username" : "Benutzername:", @@ -188,9 +190,7 @@ "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", "vcmi.server.errors.modsToDisable" : "{Folgende Mods müssen deaktiviert werden}", - "vcmi.server.errors.modNoDependency" : "Mod {'%s'} konnte nicht geladen werden!\n Sie hängt von Mod {'%s'} ab, die nicht aktiv ist!\n", "vcmi.server.errors.modDependencyLoop" : "Mod {'%s'} konnte nicht geladen werden.!\n Möglicherweise befindet sie sich in einer (weichen) Abhängigkeitsschleife.", - "vcmi.server.errors.modConflict" : "Mod {'%s'} konnte nicht geladen werden!\n Konflikte mit aktiver Mod {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!", "vcmi.dimensionDoor.seaToLandError" : "Es ist nicht möglich, mit einer Dimensionstür vom Meer zum Land oder umgekehrt zu teleportieren.", diff --git a/Mods/vcmi/config/polish.json b/Mods/vcmi/Content/config/polish.json similarity index 99% rename from Mods/vcmi/config/polish.json rename to Mods/vcmi/Content/config/polish.json index 03e537f18..4a8eba77e 100644 --- a/Mods/vcmi/config/polish.json +++ b/Mods/vcmi/Content/config/polish.json @@ -182,9 +182,7 @@ "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", "vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}", "vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}", - "vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n", "vcmi.server.errors.modDependencyLoop" : "Nie udało się wczytać moda {'%s'}!\n Być może znajduje się w pętli zależności", - "vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!", "vcmi.dimensionDoor.seaToLandError" : "Nie jest możliwa teleportacja przez drzwi wymiarów z wód na ląd i na odwrót.", diff --git a/Mods/vcmi/config/portuguese.json b/Mods/vcmi/Content/config/portuguese.json similarity index 91% rename from Mods/vcmi/config/portuguese.json rename to Mods/vcmi/Content/config/portuguese.json index 4185649af..025375d4c 100644 --- a/Mods/vcmi/config/portuguese.json +++ b/Mods/vcmi/Content/config/portuguese.json @@ -28,6 +28,13 @@ "vcmi.adventureMap.movementPointsHeroInfo" : "(Pontos de movimento: %REMAINING / %POINTS)", "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Desculpe, a repetição do turno do oponente ainda não está implementada!", + "vcmi.bonusSource.artifact" : "Artefato", + "vcmi.bonusSource.creature" : "Habilidade", + "vcmi.bonusSource.spell" : "Feitiço", + "vcmi.bonusSource.hero" : "Herói", + "vcmi.bonusSource.commander" : "Comandante", + "vcmi.bonusSource.other" : "Outro", + "vcmi.capitalColors.0" : "Vermelho", "vcmi.capitalColors.1" : "Azul", "vcmi.capitalColors.2" : "Bege", @@ -85,6 +92,7 @@ "vcmi.spellResearch.research" : "Pesquisar este Feitiço", "vcmi.spellResearch.skip" : "Pular este Feitiço", "vcmi.spellResearch.abort" : "Abortar", + "vcmi.spellResearch.noMoreSpells" : "Não há mais feitiços disponíveis para pesquisa.", "vcmi.mainMenu.serverConnecting" : "Conectando...", "vcmi.mainMenu.serverAddressEnter" : "Insira o endereço:", @@ -96,16 +104,60 @@ "vcmi.lobby.filepath" : "Caminho do arquivo", "vcmi.lobby.creationDate" : "Data de criação", "vcmi.lobby.scenarioName" : "Nome do cenário", - "vcmi.lobby.mapPreview" : "Visualização do mapa", - "vcmi.lobby.noPreview" : "sem visualização", + "vcmi.lobby.mapPreview" : "Prévia do mapa", + "vcmi.lobby.noPreview" : "sem prévia", "vcmi.lobby.noUnderground" : "sem subterrâneo", - "vcmi.lobby.sortDate" : "Classifica mapas por data de alteração", + "vcmi.lobby.sortDate" : "Ordenar mapas por data de alteração", "vcmi.lobby.backToLobby" : "Voltar para a sala de espera", "vcmi.lobby.author" : "Autor", "vcmi.lobby.handicap" : "Desvant.", "vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).", "vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.", "vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.", + "vcmi.lobby.deleteUnsupportedSave" : "{Jogos salvos incompatíveis encontrados}\n\nO VCMI encontrou %d jogos salvos que não são mais compatíveis, possivelmente devido a diferenças nas versões do VCMI.\n\nVocê deseja excluí-los?", + "vcmi.lobby.deleteSaveGameTitle" : "Selecione um Jogo Salvo para excluir", + "vcmi.lobby.deleteMapTitle" : "Selecione um Cenário para excluir", + "vcmi.lobby.deleteFile" : "Deseja excluir o seguinte arquivo?", + "vcmi.lobby.deleteFolder" : "Deseja excluir a seguinte pasta?", + "vcmi.lobby.deleteMode" : "Alternar para o modo de exclusão e voltar", + + "vcmi.broadcast.failedLoadGame" : "Falha ao carregar o jogo", + "vcmi.broadcast.command" : "Use '!help' para listar os comandos disponíveis", + "vcmi.broadcast.simturn.end" : "Os turnos simultâneos terminaram", + "vcmi.broadcast.simturn.endBetween" : "Os turnos simultâneos entre os jogadores %s e %s terminaram", + "vcmi.broadcast.serverProblem" : "O servidor encontrou um problema", + "vcmi.broadcast.gameTerminated" : "o jogo foi encerrado", + "vcmi.broadcast.gameSavedAs" : "jogo salvo como", + "vcmi.broadcast.noCheater" : "Nenhum trapaçeiro registrado!", + "vcmi.broadcast.playerCheater" : "O jogador %s é um trapaçeiro!", + "vcmi.broadcast.statisticFile" : "Os arquivos de estatísticas podem ser encontrados no diretório %s", + "vcmi.broadcast.help.commands" : "Comandos disponíveis para o anfitrião:", + "vcmi.broadcast.help.exit" : "'!exit' - termina imediatamente o jogo atual", + "vcmi.broadcast.help.kick" : "'!kick ' - expulsa o jogador especificado do jogo", + "vcmi.broadcast.help.save" : "'!save ' - salva o jogo com o nome de arquivo especificado", + "vcmi.broadcast.help.statistic" : "'!statistic' - salva as estatísticas do jogo como arquivo csv", + "vcmi.broadcast.help.commandsAll" : "Comandos disponíveis para todos os jogadores:", + "vcmi.broadcast.help.help" : "'!help' - exibe esta ajuda", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - lista os jogadores que usaram comandos de trapaça durante o jogo", + "vcmi.broadcast.help.vote" : "'!vote' - permite mudar algumas configurações do jogo se todos os jogadores votarem a favor", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - permite turnos simultâneos por um número determinado de dias, ou até o contato", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - força turnos simultâneos por um número determinado de dias, bloqueando os contatos dos jogadores", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - aborta os turnos simultâneos assim que este turno terminar", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolonga o temporizador base para todos os jogadores por um número determinado de segundos", + "vcmi.broadcast.vote.noActive" : "Nenhuma votação ativa!", + "vcmi.broadcast.vote.yes" : "sim", + "vcmi.broadcast.vote.no" : "não", + "vcmi.broadcast.vote.notRecognized" : "Comando de votação não reconhecido!", + "vcmi.broadcast.vote.success.untilContacts" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias, ou até o contato", + "vcmi.broadcast.vote.success.contactsBlocked" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias. Os contatos estão bloqueados", + "vcmi.broadcast.vote.success.nextDay" : "Votação bem-sucedida. Os turnos simultâneos terminarão no próximo dia", + "vcmi.broadcast.vote.success.timer" : "Votação bem-sucedida. O temporizador para todos os jogadores foi prolongado por %s segundos", + "vcmi.broadcast.vote.aborted" : "O jogador votou contra a mudança. Votação abortada", + "vcmi.broadcast.vote.start.untilContacts" : "Iniciada votação para permitir turnos simultâneos por mais %s dias", + "vcmi.broadcast.vote.start.contactsBlocked" : "Iniciada votação para forçar turnos simultâneos por mais %s dias", + "vcmi.broadcast.vote.start.nextDay" : "Iniciada votação para terminar os turnos simultâneos a partir do próximo dia", + "vcmi.broadcast.vote.start.timer" : "Iniciada votação para prolongar o temporizador para todos os jogadores por %s segundos", + "vcmi.broadcast.vote.hint" : "Digite '!vote yes' para concordar com esta mudança ou '!vote no' para votar contra", "vcmi.lobby.login.title" : "Sala de Espera Online do VCMI", "vcmi.lobby.login.username" : "Nome de usuário:", @@ -114,6 +166,7 @@ "vcmi.lobby.login.create" : "Nova Conta", "vcmi.lobby.login.login" : "Entrar", "vcmi.lobby.login.as" : "Entrar como %s", + "vcmi.lobby.login.spectator" : "Espectador", "vcmi.lobby.header.rooms" : "Salas de Jogo - %d", "vcmi.lobby.header.channels" : "Canais de Bate-papo", "vcmi.lobby.header.chat.global" : "Bate-papo Global do Jogo - %s", // %s -> nome do idioma @@ -174,9 +227,9 @@ "vcmi.server.errors.existingProcess" : "Outro processo do servidor VCMI está em execução. Por favor, termine-o antes de iniciar um novo jogo.", "vcmi.server.errors.modsToEnable" : "{Os seguintes mods são necessários}", "vcmi.server.errors.modsToDisable" : "{Os seguintes mods devem ser desativados}", - "vcmi.server.errors.modNoDependency" : "Falha ao carregar o mod {'%s'}!\n Ele depende do mod {'%s'}, que não está ativo!\n", - "vcmi.server.errors.modConflict" : "Falha ao carregar o mod {'%s'}!\n Conflito com o mod ativo {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Falha ao carregar o jogo salvo! Entidade desconhecida '%s' encontrada no jogo salvo! O jogo salvo pode não ser compatível com a versão atualmente instalada dos mods!", + "vcmi.server.errors.wrongIdentified" : "Você foi identificado como jogador %s, enquanto se espera %s", + "vcmi.server.errors.notAllowed" : "Você não tem permissão para realizar esta ação!", "vcmi.dimensionDoor.seaToLandError" : "Não é possível teleportar do mar para a terra ou vice-versa com uma Porta Dimensional.", @@ -610,7 +663,7 @@ "core.bonus.FEROCITY.description" : "Ataca ${val} vezes adicionais se matar alguém", "core.bonus.FLYING.name" : "Voo", "core.bonus.FLYING.description" : "Voa ao se mover (ignora obstáculos)", - "core.bonus.FREE_SHOOTING.name" : "Tiro Livre", + "core.bonus.FREE_SHOOTING.name" : "Tiro Curto", "core.bonus.FREE_SHOOTING.description" : "Pode usar ataques à distância em combate corpo a corpo", "core.bonus.GARGOYLE.name" : "Gárgula", "core.bonus.GARGOYLE.description" : "Não pode ser levantado ou curado", @@ -629,7 +682,7 @@ "core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Imune a Feitiços 1-${val}", "core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Imunidade a feitiços dos níveis 1-${val}", "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Alcance de Tiro Limitado", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a uma distância maior que ${val} hexágonos", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a mais de ${val} hexágonos de distância", "core.bonus.LIFE_DRAIN.name" : "Drenar Vida (${val}%)", "core.bonus.LIFE_DRAIN.description" : "Drena ${val}% do dano causado", "core.bonus.MANA_CHANNELING.name" : "Canalização Mágica ${val}%", @@ -706,9 +759,10 @@ "core.bonus.DISINTEGRATE.description": "Nenhum corpo permanece após a morte", "core.bonus.INVINCIBLE.name": "Invencível", "core.bonus.INVINCIBLE.description": "Não pode ser afetado por nada", + "core.bonus.MECHANICAL.name": "Mecânico", + "core.bonus.MECHANICAL.description": "Imunidade a muitos efeitos, reparável", "core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Sopro Prismático", "core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Ataque de Sopro Prismático (três direções)", - "vcmi.server.errors.modDependencyLoop" : "Falha ao carregar o mod {'%s'}!\n Ele pode estar em um ciclo de dependência.", "spell.core.castleMoat.name": "Fosso", "spell.core.castleMoatTrigger.name": "Fosso", diff --git a/Mods/vcmi/config/rmg/hdmod/aroundamarsh.json b/Mods/vcmi/Content/config/rmg/hdmod/aroundamarsh.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/aroundamarsh.json rename to Mods/vcmi/Content/config/rmg/hdmod/aroundamarsh.json diff --git a/Mods/vcmi/config/rmg/hdmod/balance.json b/Mods/vcmi/Content/config/rmg/hdmod/balance.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/balance.json rename to Mods/vcmi/Content/config/rmg/hdmod/balance.json diff --git a/Mods/vcmi/config/rmg/hdmod/blockbuster.json b/Mods/vcmi/Content/config/rmg/hdmod/blockbuster.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/blockbuster.json rename to Mods/vcmi/Content/config/rmg/hdmod/blockbuster.json diff --git a/Mods/vcmi/config/rmg/hdmod/clashOfDragons.json b/Mods/vcmi/Content/config/rmg/hdmod/clashOfDragons.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/clashOfDragons.json rename to Mods/vcmi/Content/config/rmg/hdmod/clashOfDragons.json diff --git a/Mods/vcmi/config/rmg/hdmod/coldshadowsFantasy.json b/Mods/vcmi/Content/config/rmg/hdmod/coldshadowsFantasy.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/coldshadowsFantasy.json rename to Mods/vcmi/Content/config/rmg/hdmod/coldshadowsFantasy.json diff --git a/Mods/vcmi/config/rmg/hdmod/cube.json b/Mods/vcmi/Content/config/rmg/hdmod/cube.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/cube.json rename to Mods/vcmi/Content/config/rmg/hdmod/cube.json diff --git a/Mods/vcmi/config/rmg/hdmod/diamond.json b/Mods/vcmi/Content/config/rmg/hdmod/diamond.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/diamond.json rename to Mods/vcmi/Content/config/rmg/hdmod/diamond.json diff --git a/Mods/vcmi/config/rmg/hdmod/extreme.json b/Mods/vcmi/Content/config/rmg/hdmod/extreme.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/extreme.json rename to Mods/vcmi/Content/config/rmg/hdmod/extreme.json diff --git a/Mods/vcmi/config/rmg/hdmod/extreme2.json b/Mods/vcmi/Content/config/rmg/hdmod/extreme2.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/extreme2.json rename to Mods/vcmi/Content/config/rmg/hdmod/extreme2.json diff --git a/Mods/vcmi/config/rmg/hdmod/fear.json b/Mods/vcmi/Content/config/rmg/hdmod/fear.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/fear.json rename to Mods/vcmi/Content/config/rmg/hdmod/fear.json diff --git a/Mods/vcmi/config/rmg/hdmod/frozenDragons.json b/Mods/vcmi/Content/config/rmg/hdmod/frozenDragons.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/frozenDragons.json rename to Mods/vcmi/Content/config/rmg/hdmod/frozenDragons.json diff --git a/Mods/vcmi/config/rmg/hdmod/gimlisRevenge.json b/Mods/vcmi/Content/config/rmg/hdmod/gimlisRevenge.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/gimlisRevenge.json rename to Mods/vcmi/Content/config/rmg/hdmod/gimlisRevenge.json diff --git a/Mods/vcmi/config/rmg/hdmod/guerilla.json b/Mods/vcmi/Content/config/rmg/hdmod/guerilla.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/guerilla.json rename to Mods/vcmi/Content/config/rmg/hdmod/guerilla.json diff --git a/Mods/vcmi/config/rmg/hdmod/headquarters.json b/Mods/vcmi/Content/config/rmg/hdmod/headquarters.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/headquarters.json rename to Mods/vcmi/Content/config/rmg/hdmod/headquarters.json diff --git a/Mods/vcmi/config/rmg/hdmod/hypercube.json b/Mods/vcmi/Content/config/rmg/hdmod/hypercube.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/hypercube.json rename to Mods/vcmi/Content/config/rmg/hdmod/hypercube.json diff --git a/Mods/vcmi/config/rmg/hdmod/jebusCross.json b/Mods/vcmi/Content/config/rmg/hdmod/jebusCross.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/jebusCross.json rename to Mods/vcmi/Content/config/rmg/hdmod/jebusCross.json diff --git a/Mods/vcmi/config/rmg/hdmod/longRun.json b/Mods/vcmi/Content/config/rmg/hdmod/longRun.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/longRun.json rename to Mods/vcmi/Content/config/rmg/hdmod/longRun.json diff --git a/Mods/vcmi/config/rmg/hdmod/marathon.json b/Mods/vcmi/Content/config/rmg/hdmod/marathon.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/marathon.json rename to Mods/vcmi/Content/config/rmg/hdmod/marathon.json diff --git a/Mods/vcmi/config/rmg/hdmod/miniNostalgia.json b/Mods/vcmi/Content/config/rmg/hdmod/miniNostalgia.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/miniNostalgia.json rename to Mods/vcmi/Content/config/rmg/hdmod/miniNostalgia.json diff --git a/Mods/vcmi/config/rmg/hdmod/nostalgia.json b/Mods/vcmi/Content/config/rmg/hdmod/nostalgia.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/nostalgia.json rename to Mods/vcmi/Content/config/rmg/hdmod/nostalgia.json diff --git a/Mods/vcmi/config/rmg/hdmod/oceansEleven.json b/Mods/vcmi/Content/config/rmg/hdmod/oceansEleven.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/oceansEleven.json rename to Mods/vcmi/Content/config/rmg/hdmod/oceansEleven.json diff --git a/Mods/vcmi/config/rmg/hdmod/panic.json b/Mods/vcmi/Content/config/rmg/hdmod/panic.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/panic.json rename to Mods/vcmi/Content/config/rmg/hdmod/panic.json diff --git a/Mods/vcmi/config/rmg/hdmod/poorJebus.json b/Mods/vcmi/Content/config/rmg/hdmod/poorJebus.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/poorJebus.json rename to Mods/vcmi/Content/config/rmg/hdmod/poorJebus.json diff --git a/Mods/vcmi/config/rmg/hdmod/reckless.json b/Mods/vcmi/Content/config/rmg/hdmod/reckless.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/reckless.json rename to Mods/vcmi/Content/config/rmg/hdmod/reckless.json diff --git a/Mods/vcmi/config/rmg/hdmod/roadrunner.json b/Mods/vcmi/Content/config/rmg/hdmod/roadrunner.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/roadrunner.json rename to Mods/vcmi/Content/config/rmg/hdmod/roadrunner.json diff --git a/Mods/vcmi/config/rmg/hdmod/shaaafworld.json b/Mods/vcmi/Content/config/rmg/hdmod/shaaafworld.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/shaaafworld.json rename to Mods/vcmi/Content/config/rmg/hdmod/shaaafworld.json diff --git a/Mods/vcmi/config/rmg/hdmod/skirmish.json b/Mods/vcmi/Content/config/rmg/hdmod/skirmish.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/skirmish.json rename to Mods/vcmi/Content/config/rmg/hdmod/skirmish.json diff --git a/Mods/vcmi/config/rmg/hdmod/speed1.json b/Mods/vcmi/Content/config/rmg/hdmod/speed1.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/speed1.json rename to Mods/vcmi/Content/config/rmg/hdmod/speed1.json diff --git a/Mods/vcmi/config/rmg/hdmod/speed2.json b/Mods/vcmi/Content/config/rmg/hdmod/speed2.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/speed2.json rename to Mods/vcmi/Content/config/rmg/hdmod/speed2.json diff --git a/Mods/vcmi/config/rmg/hdmod/spider.json b/Mods/vcmi/Content/config/rmg/hdmod/spider.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/spider.json rename to Mods/vcmi/Content/config/rmg/hdmod/spider.json diff --git a/Mods/vcmi/config/rmg/hdmod/superslam.json b/Mods/vcmi/Content/config/rmg/hdmod/superslam.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/superslam.json rename to Mods/vcmi/Content/config/rmg/hdmod/superslam.json diff --git a/Mods/vcmi/config/rmg/hdmod/triad.json b/Mods/vcmi/Content/config/rmg/hdmod/triad.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/triad.json rename to Mods/vcmi/Content/config/rmg/hdmod/triad.json diff --git a/Mods/vcmi/config/rmg/hdmod/vortex.json b/Mods/vcmi/Content/config/rmg/hdmod/vortex.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmod/vortex.json rename to Mods/vcmi/Content/config/rmg/hdmod/vortex.json diff --git a/Mods/vcmi/config/rmg/hdmodUnused/anarchy.json b/Mods/vcmi/Content/config/rmg/hdmodUnused/anarchy.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmodUnused/anarchy.json rename to Mods/vcmi/Content/config/rmg/hdmodUnused/anarchy.json diff --git a/Mods/vcmi/config/rmg/hdmodUnused/balance m+u 200%.json b/Mods/vcmi/Content/config/rmg/hdmodUnused/balance m+u 200%.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmodUnused/balance m+u 200%.json rename to Mods/vcmi/Content/config/rmg/hdmodUnused/balance m+u 200%.json diff --git a/Mods/vcmi/config/rmg/hdmodUnused/midnightMix.json b/Mods/vcmi/Content/config/rmg/hdmodUnused/midnightMix.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmodUnused/midnightMix.json rename to Mods/vcmi/Content/config/rmg/hdmodUnused/midnightMix.json diff --git a/Mods/vcmi/config/rmg/hdmodUnused/skirmish m-u 200%.json b/Mods/vcmi/Content/config/rmg/hdmodUnused/skirmish m-u 200%.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmodUnused/skirmish m-u 200%.json rename to Mods/vcmi/Content/config/rmg/hdmodUnused/skirmish m-u 200%.json diff --git a/Mods/vcmi/config/rmg/hdmodUnused/true random.json b/Mods/vcmi/Content/config/rmg/hdmodUnused/true random.json similarity index 100% rename from Mods/vcmi/config/rmg/hdmodUnused/true random.json rename to Mods/vcmi/Content/config/rmg/hdmodUnused/true random.json diff --git a/Mods/vcmi/config/rmg/heroes3/dwarvenTunnels.json b/Mods/vcmi/Content/config/rmg/heroes3/dwarvenTunnels.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3/dwarvenTunnels.json rename to Mods/vcmi/Content/config/rmg/heroes3/dwarvenTunnels.json diff --git a/Mods/vcmi/config/rmg/heroes3/golemsAplenty.json b/Mods/vcmi/Content/config/rmg/heroes3/golemsAplenty.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3/golemsAplenty.json rename to Mods/vcmi/Content/config/rmg/heroes3/golemsAplenty.json diff --git a/Mods/vcmi/config/rmg/heroes3/meetingInMuzgob.json b/Mods/vcmi/Content/config/rmg/heroes3/meetingInMuzgob.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3/meetingInMuzgob.json rename to Mods/vcmi/Content/config/rmg/heroes3/meetingInMuzgob.json diff --git a/Mods/vcmi/config/rmg/heroes3/monksRetreat.json b/Mods/vcmi/Content/config/rmg/heroes3/monksRetreat.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3/monksRetreat.json rename to Mods/vcmi/Content/config/rmg/heroes3/monksRetreat.json diff --git a/Mods/vcmi/config/rmg/heroes3/newcomers.json b/Mods/vcmi/Content/config/rmg/heroes3/newcomers.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3/newcomers.json rename to Mods/vcmi/Content/config/rmg/heroes3/newcomers.json diff --git a/Mods/vcmi/config/rmg/heroes3/readyOrNot.json b/Mods/vcmi/Content/config/rmg/heroes3/readyOrNot.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3/readyOrNot.json rename to Mods/vcmi/Content/config/rmg/heroes3/readyOrNot.json diff --git a/Mods/vcmi/config/rmg/heroes3/smallRing.json b/Mods/vcmi/Content/config/rmg/heroes3/smallRing.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3/smallRing.json rename to Mods/vcmi/Content/config/rmg/heroes3/smallRing.json diff --git a/Mods/vcmi/config/rmg/heroes3/southOfHell.json b/Mods/vcmi/Content/config/rmg/heroes3/southOfHell.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3/southOfHell.json rename to Mods/vcmi/Content/config/rmg/heroes3/southOfHell.json diff --git a/Mods/vcmi/config/rmg/heroes3/worldsAtWar.json b/Mods/vcmi/Content/config/rmg/heroes3/worldsAtWar.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3/worldsAtWar.json rename to Mods/vcmi/Content/config/rmg/heroes3/worldsAtWar.json diff --git a/Mods/vcmi/config/rmg/heroes3unused/dragon.json b/Mods/vcmi/Content/config/rmg/heroes3unused/dragon.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3unused/dragon.json rename to Mods/vcmi/Content/config/rmg/heroes3unused/dragon.json diff --git a/Mods/vcmi/config/rmg/heroes3unused/gauntlet.json b/Mods/vcmi/Content/config/rmg/heroes3unused/gauntlet.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3unused/gauntlet.json rename to Mods/vcmi/Content/config/rmg/heroes3unused/gauntlet.json diff --git a/Mods/vcmi/config/rmg/heroes3unused/ring.json b/Mods/vcmi/Content/config/rmg/heroes3unused/ring.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3unused/ring.json rename to Mods/vcmi/Content/config/rmg/heroes3unused/ring.json diff --git a/Mods/vcmi/config/rmg/heroes3unused/riseOfPhoenix.json b/Mods/vcmi/Content/config/rmg/heroes3unused/riseOfPhoenix.json similarity index 100% rename from Mods/vcmi/config/rmg/heroes3unused/riseOfPhoenix.json rename to Mods/vcmi/Content/config/rmg/heroes3unused/riseOfPhoenix.json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm0k.json b/Mods/vcmi/Content/config/rmg/symmetric/2sm0k.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm0k.json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm0k.json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2a.json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2a.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2a.json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2a.json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2b(2).json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2b(2).json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2b(2).json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2b(2).json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2b.json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2b.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2b.json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2b.json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2c.json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2c.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2c.json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2c.json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2f(2).json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2f(2).json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2f(2).json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2f(2).json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2f.json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2f.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2f.json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2f.json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2h(2).json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2h(2).json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2h(2).json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2h(2).json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2h.json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2h.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2h.json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2h.json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2i(2).json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2i(2).json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2i(2).json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2i(2).json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm2i.json b/Mods/vcmi/Content/config/rmg/symmetric/2sm2i.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm2i.json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2i.json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm4d(2).json b/Mods/vcmi/Content/config/rmg/symmetric/2sm4d(2).json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm4d(2).json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm4d(2).json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm4d(3).json b/Mods/vcmi/Content/config/rmg/symmetric/2sm4d(3).json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm4d(3).json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm4d(3).json diff --git a/Mods/vcmi/config/rmg/symmetric/2sm4d.json b/Mods/vcmi/Content/config/rmg/symmetric/2sm4d.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/2sm4d.json rename to Mods/vcmi/Content/config/rmg/symmetric/2sm4d.json diff --git a/Mods/vcmi/config/rmg/symmetric/3sb0b.json b/Mods/vcmi/Content/config/rmg/symmetric/3sb0b.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/3sb0b.json rename to Mods/vcmi/Content/config/rmg/symmetric/3sb0b.json diff --git a/Mods/vcmi/config/rmg/symmetric/3sb0c.json b/Mods/vcmi/Content/config/rmg/symmetric/3sb0c.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/3sb0c.json rename to Mods/vcmi/Content/config/rmg/symmetric/3sb0c.json diff --git a/Mods/vcmi/config/rmg/symmetric/3sm3d.json b/Mods/vcmi/Content/config/rmg/symmetric/3sm3d.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/3sm3d.json rename to Mods/vcmi/Content/config/rmg/symmetric/3sm3d.json diff --git a/Mods/vcmi/config/rmg/symmetric/4sm0d.json b/Mods/vcmi/Content/config/rmg/symmetric/4sm0d.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/4sm0d.json rename to Mods/vcmi/Content/config/rmg/symmetric/4sm0d.json diff --git a/Mods/vcmi/config/rmg/symmetric/4sm0f.json b/Mods/vcmi/Content/config/rmg/symmetric/4sm0f.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/4sm0f.json rename to Mods/vcmi/Content/config/rmg/symmetric/4sm0f.json diff --git a/Mods/vcmi/config/rmg/symmetric/4sm0g.json b/Mods/vcmi/Content/config/rmg/symmetric/4sm0g.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/4sm0g.json rename to Mods/vcmi/Content/config/rmg/symmetric/4sm0g.json diff --git a/Mods/vcmi/config/rmg/symmetric/4sm4e.json b/Mods/vcmi/Content/config/rmg/symmetric/4sm4e.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/4sm4e.json rename to Mods/vcmi/Content/config/rmg/symmetric/4sm4e.json diff --git a/Mods/vcmi/config/rmg/symmetric/5sb0a.json b/Mods/vcmi/Content/config/rmg/symmetric/5sb0a.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/5sb0a.json rename to Mods/vcmi/Content/config/rmg/symmetric/5sb0a.json diff --git a/Mods/vcmi/config/rmg/symmetric/5sb0b.json b/Mods/vcmi/Content/config/rmg/symmetric/5sb0b.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/5sb0b.json rename to Mods/vcmi/Content/config/rmg/symmetric/5sb0b.json diff --git a/Mods/vcmi/config/rmg/symmetric/6lm10.json b/Mods/vcmi/Content/config/rmg/symmetric/6lm10.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/6lm10.json rename to Mods/vcmi/Content/config/rmg/symmetric/6lm10.json diff --git a/Mods/vcmi/config/rmg/symmetric/6lm10a.json b/Mods/vcmi/Content/config/rmg/symmetric/6lm10a.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/6lm10a.json rename to Mods/vcmi/Content/config/rmg/symmetric/6lm10a.json diff --git a/Mods/vcmi/config/rmg/symmetric/6sm0b.json b/Mods/vcmi/Content/config/rmg/symmetric/6sm0b.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/6sm0b.json rename to Mods/vcmi/Content/config/rmg/symmetric/6sm0b.json diff --git a/Mods/vcmi/config/rmg/symmetric/6sm0d.json b/Mods/vcmi/Content/config/rmg/symmetric/6sm0d.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/6sm0d.json rename to Mods/vcmi/Content/config/rmg/symmetric/6sm0d.json diff --git a/Mods/vcmi/config/rmg/symmetric/6sm0e.json b/Mods/vcmi/Content/config/rmg/symmetric/6sm0e.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/6sm0e.json rename to Mods/vcmi/Content/config/rmg/symmetric/6sm0e.json diff --git a/Mods/vcmi/config/rmg/symmetric/7sb0b.json b/Mods/vcmi/Content/config/rmg/symmetric/7sb0b.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/7sb0b.json rename to Mods/vcmi/Content/config/rmg/symmetric/7sb0b.json diff --git a/Mods/vcmi/config/rmg/symmetric/7sb0c.json b/Mods/vcmi/Content/config/rmg/symmetric/7sb0c.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/7sb0c.json rename to Mods/vcmi/Content/config/rmg/symmetric/7sb0c.json diff --git a/Mods/vcmi/config/rmg/symmetric/8mm0e.json b/Mods/vcmi/Content/config/rmg/symmetric/8mm0e.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/8mm0e.json rename to Mods/vcmi/Content/config/rmg/symmetric/8mm0e.json diff --git a/Mods/vcmi/config/rmg/symmetric/8mm6.json b/Mods/vcmi/Content/config/rmg/symmetric/8mm6.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/8mm6.json rename to Mods/vcmi/Content/config/rmg/symmetric/8mm6.json diff --git a/Mods/vcmi/config/rmg/symmetric/8mm6a.json b/Mods/vcmi/Content/config/rmg/symmetric/8mm6a.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/8mm6a.json rename to Mods/vcmi/Content/config/rmg/symmetric/8mm6a.json diff --git a/Mods/vcmi/config/rmg/symmetric/8sm0c.json b/Mods/vcmi/Content/config/rmg/symmetric/8sm0c.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/8sm0c.json rename to Mods/vcmi/Content/config/rmg/symmetric/8sm0c.json diff --git a/Mods/vcmi/config/rmg/symmetric/8sm0f.json b/Mods/vcmi/Content/config/rmg/symmetric/8sm0f.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/8sm0f.json rename to Mods/vcmi/Content/config/rmg/symmetric/8sm0f.json diff --git a/Mods/vcmi/config/rmg/symmetric/8xm12.json b/Mods/vcmi/Content/config/rmg/symmetric/8xm12.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/8xm12.json rename to Mods/vcmi/Content/config/rmg/symmetric/8xm12.json diff --git a/Mods/vcmi/config/rmg/symmetric/8xm12a.json b/Mods/vcmi/Content/config/rmg/symmetric/8xm12a.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/8xm12a.json rename to Mods/vcmi/Content/config/rmg/symmetric/8xm12a.json diff --git a/Mods/vcmi/config/rmg/symmetric/8xm8.json b/Mods/vcmi/Content/config/rmg/symmetric/8xm8.json similarity index 100% rename from Mods/vcmi/config/rmg/symmetric/8xm8.json rename to Mods/vcmi/Content/config/rmg/symmetric/8xm8.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/2mm2h.json b/Mods/vcmi/Content/config/rmg/unknownUnused/2mm2h.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/2mm2h.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/2mm2h.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/2x2sm4d(3).json b/Mods/vcmi/Content/config/rmg/unknownUnused/2x2sm4d(3).json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/2x2sm4d(3).json rename to Mods/vcmi/Content/config/rmg/unknownUnused/2x2sm4d(3).json diff --git a/Mods/vcmi/config/rmg/unknownUnused/4mm2h.json b/Mods/vcmi/Content/config/rmg/unknownUnused/4mm2h.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/4mm2h.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/4mm2h.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/4sm3i.json b/Mods/vcmi/Content/config/rmg/unknownUnused/4sm3i.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/4sm3i.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/4sm3i.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/6lm10a.json b/Mods/vcmi/Content/config/rmg/unknownUnused/6lm10a.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/6lm10a.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/6lm10a.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/8xm12 huge.json b/Mods/vcmi/Content/config/rmg/unknownUnused/8xm12 huge.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/8xm12 huge.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/8xm12 huge.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/8xm8 huge.json b/Mods/vcmi/Content/config/rmg/unknownUnused/8xm8 huge.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/8xm8 huge.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/8xm8 huge.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/analogy.json b/Mods/vcmi/Content/config/rmg/unknownUnused/analogy.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/analogy.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/analogy.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/cross.json b/Mods/vcmi/Content/config/rmg/unknownUnused/cross.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/cross.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/cross.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/cross2.json b/Mods/vcmi/Content/config/rmg/unknownUnused/cross2.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/cross2.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/cross2.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/cross3.json b/Mods/vcmi/Content/config/rmg/unknownUnused/cross3.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/cross3.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/cross3.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/deux paires.json b/Mods/vcmi/Content/config/rmg/unknownUnused/deux paires.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/deux paires.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/deux paires.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/doubled 8mm6.json b/Mods/vcmi/Content/config/rmg/unknownUnused/doubled 8mm6.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/doubled 8mm6.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/doubled 8mm6.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/elka.json b/Mods/vcmi/Content/config/rmg/unknownUnused/elka.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/elka.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/elka.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/goldenRing.json b/Mods/vcmi/Content/config/rmg/unknownUnused/goldenRing.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/goldenRing.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/goldenRing.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/greatSands.json b/Mods/vcmi/Content/config/rmg/unknownUnused/greatSands.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/greatSands.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/greatSands.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/kite.json b/Mods/vcmi/Content/config/rmg/unknownUnused/kite.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/kite.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/kite.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/upgrade.json b/Mods/vcmi/Content/config/rmg/unknownUnused/upgrade.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/upgrade.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/upgrade.json diff --git a/Mods/vcmi/config/rmg/unknownUnused/wheel.json b/Mods/vcmi/Content/config/rmg/unknownUnused/wheel.json similarity index 100% rename from Mods/vcmi/config/rmg/unknownUnused/wheel.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/wheel.json diff --git a/Mods/vcmi/config/russian.json b/Mods/vcmi/Content/config/russian.json similarity index 100% rename from Mods/vcmi/config/russian.json rename to Mods/vcmi/Content/config/russian.json diff --git a/Mods/vcmi/config/spanish.json b/Mods/vcmi/Content/config/spanish.json similarity index 99% rename from Mods/vcmi/config/spanish.json rename to Mods/vcmi/Content/config/spanish.json index c4f4c5215..f1e82d286 100644 --- a/Mods/vcmi/config/spanish.json +++ b/Mods/vcmi/Content/config/spanish.json @@ -79,8 +79,6 @@ "vcmi.server.errors.modsToEnable" : "{Se requieren los siguientes mods}", "vcmi.server.errors.modsToDisable" : "{Deben desactivarse los siguientes mods}", "vcmi.server.confirmReconnect" : "¿Quieres reconectar a la última sesión?", - "vcmi.server.errors.modNoDependency" : "Error al cargar el mod {'%s'}.\n Depende del mod {'%s'}, que no está activo.\n", - "vcmi.server.errors.modConflict" : "Error al cargar el mod {'%s'}.\n Conflicto con el mod activo {'%s'}.\n", "vcmi.server.errors.unknownEntity" : "Error al cargar la partida guardada. ¡Se encontró una entidad desconocida '%s' en la partida guardada! Es posible que la partida no sea compatible con la versión actualmente instalada de los mods.", "vcmi.settingsMainWindow.generalTab.hover" : "General", diff --git a/Mods/vcmi/config/spells.json b/Mods/vcmi/Content/config/spells.json similarity index 100% rename from Mods/vcmi/config/spells.json rename to Mods/vcmi/Content/config/spells.json diff --git a/Mods/vcmi/config/swedish.json b/Mods/vcmi/Content/config/swedish.json similarity index 89% rename from Mods/vcmi/config/swedish.json rename to Mods/vcmi/Content/config/swedish.json index e981350dc..868d39b87 100644 --- a/Mods/vcmi/config/swedish.json +++ b/Mods/vcmi/Content/config/swedish.json @@ -28,6 +28,13 @@ "vcmi.adventureMap.movementPointsHeroInfo" : "(Förflyttningspoäng: %REMAINING / %POINTS)", "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Tyvärr, att spela om motståndarens tur är inte implementerat ännu!", + "vcmi.bonusSource.artifact" : "Artefakt", + "vcmi.bonusSource.creature" : "Förmåga", + "vcmi.bonusSource.spell" : "Trollformel", + "vcmi.bonusSource.hero" : "Hjälte", + "vcmi.bonusSource.commander": "Befälhavare", + "vcmi.bonusSource.other" : "Annan", + "vcmi.capitalColors.0" : "Röd", "vcmi.capitalColors.1" : "Blå", "vcmi.capitalColors.2" : "Ljusbrun", @@ -42,6 +49,12 @@ "vcmi.heroOverview.secondarySkills" : "Sekundärförmågor", "vcmi.heroOverview.spells" : "Trollformler", + "vcmi.quickExchange.moveUnit" : "Flytta enhet", + "vcmi.quickExchange.moveAllUnits" : "Flytta alla enheter", + "vcmi.quickExchange.swapAllUnits" : "Byt arméer", + "vcmi.quickExchange.moveAllArtifacts": "Flytta alla artefakter", + "vcmi.quickExchange.swapAllArtifacts": "Byt artefakter", + "vcmi.radialWheel.mergeSameUnit" : "Slå samman varelser av samma sort", "vcmi.radialWheel.fillSingleUnit" : "Fyll på med enstaka varelser", "vcmi.radialWheel.splitSingleUnit" : "Dela av en enda varelse", @@ -61,6 +74,16 @@ "vcmi.radialWheel.moveDown" : "Flytta nedåt", "vcmi.radialWheel.moveBottom" : "Flytta längst ner", + "vcmi.randomMap.description" : "Kartan skapades av den slumpmässiga kartgeneratorn.\nMallen var %s, storlek %dx%d, nivåer %d, spelare %d, datorspelare %d, vatten %s, monster %s, VCMI karta", + "vcmi.randomMap.description.isHuman" : ", %s är människa", + "vcmi.randomMap.description.townChoice" : ", %s valde stadstyp: %s", + "vcmi.randomMap.description.water.none" : "inget", + "vcmi.randomMap.description.water.normal" : "normalt", + "vcmi.randomMap.description.water.islands" : "öar", + "vcmi.randomMap.description.monster.weak" : "svaga", + "vcmi.randomMap.description.monster.normal": "normala", + "vcmi.randomMap.description.monster.strong": "starka", + "vcmi.spellBook.search" : "sök...", "vcmi.spellResearch.canNotAfford" : "Du har inte råd att byta ut '{%SPELL1}' med '{%SPELL2}'. Du kan fortfarande göra dig av med den här trollformeln och forska vidare.", @@ -69,6 +92,7 @@ "vcmi.spellResearch.research" : "Forska fram denna trollformel", "vcmi.spellResearch.skip" : "Strunta i denna trollformel", "vcmi.spellResearch.abort" : "Avbryt", + "vcmi.spellResearch.noMoreSpells" : "Det finns inga fler trollformler tillgängliga för forskning.", "vcmi.mainMenu.serverConnecting" : "Ansluter...", "vcmi.mainMenu.serverAddressEnter" : "Ange adress:", @@ -90,6 +114,50 @@ "vcmi.lobby.handicap.resource" : "Ger spelarna lämpliga resurser att börja med utöver de normala startresurserna. Negativa värden är tillåtna men är begränsade till 0 totalt (spelaren börjar aldrig med negativa resurser).", "vcmi.lobby.handicap.income" : "Ändrar spelarens olika inkomster i procent (resultaten avrundas uppåt).", "vcmi.lobby.handicap.growth" : "Ändrar tillväxttakten för varelser i de städer som ägs av spelaren (resultaten avrundas uppåt).", + "vcmi.lobby.deleteUnsupportedSave": "{Ostödda sparningar av spel hittades}\n\nVCMI har hittat %d sparade spelfiler som inte längre stöds, möjligen på grund av skillnader i VCMI-versioner.\n\nVill du ta bort dem?", + "vcmi.lobby.deleteSaveGameTitle" : "Välj ett sparat spel som ska raderas", + "vcmi.lobby.deleteMapTitle" : "Välj ett scenario som ska raderas", + "vcmi.lobby.deleteFile" : "Vill du radera följande fil?", + "vcmi.lobby.deleteFolder" : "Vill du radera följande mapp?", + "vcmi.lobby.deleteMode" : "Växla till raderingsläge och tillbaka", + + "vcmi.broadcast.failedLoadGame" : "Misslyckades med att ladda spelet", + "vcmi.broadcast.command" : "Använd '!help' för att lista tillgängliga kommandon", + "vcmi.broadcast.simturn.end" : "Simultana turomgångar har avslutats", + "vcmi.broadcast.simturn.endBetween" : "De samtidiga turomgångarna mellan spelarna %s och %s har avslutats", + "vcmi.broadcast.serverProblem" : "Servern stötte på ett problem", + "vcmi.broadcast.gameTerminated" : "Spelet avslutades", + "vcmi.broadcast.gameSavedAs" : "Spelet sparades som", + "vcmi.broadcast.noCheater" : "Inga fuskare registrerade!", + "vcmi.broadcast.playerCheater" : "Spelare %s är fuskare!", + "vcmi.broadcast.statisticFile" : "Statistikfiler finns i %s-katalogen", + "vcmi.broadcast.help.commands" : "Tillgängliga kommandon till värden:", + "vcmi.broadcast.help.exit" : "'!exit' - avslutar omedelbart det aktuella spelet", + "vcmi.broadcast.help.kick" : "'!kick ' - sparkar ut angiven spelare från spelet", + "vcmi.broadcast.help.save" : "'!save ' - sparar spelet under angivet filnamn", + "vcmi.broadcast.help.statistic" : "'!statistic' - spara spelstatistik som csv-fil", + "vcmi.broadcast.help.commandsAll" : "Tillgängliga kommandon för alla spelare:", + "vcmi.broadcast.help.help" : "'!help' - visa den här hjälpen", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - visa lista över spelare som angav fuskkommando under spelet", + "vcmi.broadcast.help.vote" : "'!vote' - gör det möjligt att ändra vissa spelinställningar om alla spelare röstar för det", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - tillåter simultana turomgångar under angivet antal dagar, eller tills spelarnas hjältar kommer för nära varandra", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - tvingar fram simultana turomgångar under ett angivet antal dagar (blockerar spelarkontakter)", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - avbryter samtidiga turomgångar när denna turomgång avslutas", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - förlänger bastimern för alla spelare med angivet antal sekunder", + "vcmi.broadcast.vote.noActive" : "Ingen aktiv röstning!", + "vcmi.broadcast.vote.yes" : "ja", + "vcmi.broadcast.vote.no" : "nej", + "vcmi.broadcast.vote.notRecognized" : "Röstningskommando känns inte igen!", + "vcmi.broadcast.vote.success.untilContacts" : "Omröstningen lyckades. Simultana turomgångar kommer att pågå i %s dagar eller tills spelarnas hjältar kommer för nära varandra", + "vcmi.broadcast.vote.success.contactsBlocked" : "Omröstningen lyckades. Simultana turomgångar kommer att pågå i %s fler dagar. Närkontakter är blockerade", + "vcmi.broadcast.vote.success.nextDay" : "Omröstningen lyckades. Simultana turomgångar kommer att avslutas nästa dag", + "vcmi.broadcast.vote.success.timer" : "Omröstningen lyckades. Timern för alla spelare har förlängts med %s sekunder", + "vcmi.broadcast.vote.aborted" : "Spelare röstade mot förändring. Omröstningen avbröts", + "vcmi.broadcast.vote.start.untilContacts" : "Började rösta för att tillåta samtidiga turomgångar i ytterligare %s dagar", + "vcmi.broadcast.vote.start.contactsBlocked" : "Började rösta för att tvinga fram simultana turomgångar i ytterligare %s fler dagar", + "vcmi.broadcast.vote.start.nextDay" : "Började rösta för att avsluta samtidiga turomgångar från och med nästa dag", + "vcmi.broadcast.vote.start.timer" : "Började rösta för att förlänga timern för alla spelare med %s sekunder", + "vcmi.broadcast.vote.hint" : "Skriv '!vote yes' för att godkänna denna ändring eller '!vote no' för att rösta emot den", "vcmi.lobby.login.title" : "VCMI Online Lobby", "vcmi.lobby.login.username" : "Användarnamn:", @@ -98,6 +166,7 @@ "vcmi.lobby.login.create" : "Nytt konto", "vcmi.lobby.login.login" : "Logga in", "vcmi.lobby.login.as" : "Logga in som %s", + "vcmi.lobby.login.spectator" : "Åskådare", "vcmi.lobby.header.rooms" : "Spelrum - %d", "vcmi.lobby.header.channels" : "Chattkanaler", "vcmi.lobby.header.chat.global" : "Global spelchatt - %s", // %s -> språknamn @@ -158,9 +227,9 @@ "vcmi.server.errors.existingProcess" : "En annan VCMI-serverprocess är igång. Vänligen avsluta den innan du startar ett nytt spel.", "vcmi.server.errors.modsToEnable" : "{Följande modd(ar) krävs}", "vcmi.server.errors.modsToDisable" : "{Följande modd(ar) måste inaktiveras}", - "vcmi.server.errors.modNoDependency" : "Misslyckades med att ladda modd {'%s'}!\n Den är beroende av modd {'%s'} som inte är aktiverad!\n", - "vcmi.server.errors.modConflict" : "Misslyckades med att ladda modd {'%s'}!\n Konflikter med aktiverad modd {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Misslyckades med att ladda sparat spel! Okänd enhet '%s' hittades i sparat spel! Sparningen kanske inte är kompatibel med den aktuella versionen av moddarna!", + "vcmi.server.errors.wrongIdentified" : "Du identifierades som spelare %s när du förväntade dig %s", + "vcmi.server.errors.notAllowed" : "Du får inte utföra denna åtgärd!", "vcmi.dimensionDoor.seaToLandError" : "Det går inte att teleportera sig från hav till land eller tvärtom med trollformeln 'Dimensionsdörr'.", @@ -536,6 +605,8 @@ "core.seerhut.quest.reachDate.visit.4" : "Stängt fram till %s.", "core.seerhut.quest.reachDate.visit.5" : "Stängt fram till %s.", + "mapObject.core.hillFort.object.description" : "Uppgraderar varelser. Nivåerna 1 - 4 är billigare än i associerad stad.", + "core.bonus.ADDITIONAL_ATTACK.name" : "Dubbelslag", "core.bonus.ADDITIONAL_ATTACK.description" : "Attackerar två gånger.", "core.bonus.ADDITIONAL_RETALIATION.name" : "Ytterligare motattacker", @@ -687,5 +758,9 @@ "core.bonus.DISINTEGRATE.name" : "Desintegrerar", "core.bonus.DISINTEGRATE.description" : "Ingen kropp lämnas kvar på slagfältet.", "core.bonus.INVINCIBLE.name" : "Oövervinnerlig", - "core.bonus.INVINCIBLE.description" : "Kan inte påverkas av någonting." + "core.bonus.INVINCIBLE.description" : "Kan inte påverkas av någonting.", + "core.bonus.MECHANICAL.name" : "Mekanisk", + "core.bonus.MECHANICAL.description" : "Immun mot många effekter, reparerbar.", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Prism-andedräkt", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Treriktad andedräkt." } diff --git a/Mods/vcmi/config/towerCreature.json b/Mods/vcmi/Content/config/towerCreature.json similarity index 100% rename from Mods/vcmi/config/towerCreature.json rename to Mods/vcmi/Content/config/towerCreature.json diff --git a/Mods/vcmi/config/towerFactions.json b/Mods/vcmi/Content/config/towerFactions.json similarity index 100% rename from Mods/vcmi/config/towerFactions.json rename to Mods/vcmi/Content/config/towerFactions.json diff --git a/Mods/vcmi/config/ukrainian.json b/Mods/vcmi/Content/config/ukrainian.json similarity index 99% rename from Mods/vcmi/config/ukrainian.json rename to Mods/vcmi/Content/config/ukrainian.json index 22a609083..6dfde5bfe 100644 --- a/Mods/vcmi/config/ukrainian.json +++ b/Mods/vcmi/Content/config/ukrainian.json @@ -139,8 +139,6 @@ "vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}", "vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}", "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", - "vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n", - "vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!", "vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами", diff --git a/Mods/vcmi/config/vietnamese.json b/Mods/vcmi/Content/config/vietnamese.json similarity index 100% rename from Mods/vcmi/config/vietnamese.json rename to Mods/vcmi/Content/config/vietnamese.json diff --git a/Mods/vcmi/Data/settingsWindow/gear.png b/Mods/vcmi/Data/settingsWindow/gear.png deleted file mode 100644 index c5974983e..000000000 Binary files a/Mods/vcmi/Data/settingsWindow/gear.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png b/Mods/vcmi/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png deleted file mode 100644 index 5d7bfbfef..000000000 Binary files a/Mods/vcmi/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/defendBig.png b/Mods/vcmi/Sprites/vcmi/battleQueue/defendBig.png deleted file mode 100644 index ef9022ca6..000000000 Binary files a/Mods/vcmi/Sprites/vcmi/battleQueue/defendBig.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/defendSmall.png b/Mods/vcmi/Sprites/vcmi/battleQueue/defendSmall.png deleted file mode 100644 index b22a1b5d6..000000000 Binary files a/Mods/vcmi/Sprites/vcmi/battleQueue/defendSmall.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/statesBig.json b/Mods/vcmi/Sprites/vcmi/battleQueue/statesBig.json deleted file mode 100644 index e8383883c..000000000 --- a/Mods/vcmi/Sprites/vcmi/battleQueue/statesBig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "basepath": "vcmi/battleQueue/", - "images" : - [ - { "frame" : 0, "file" : "defendBig"}, - { "frame" : 1, "file" : "waitBig"} - ] -} diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/statesSmall.json b/Mods/vcmi/Sprites/vcmi/battleQueue/statesSmall.json deleted file mode 100644 index 796657130..000000000 --- a/Mods/vcmi/Sprites/vcmi/battleQueue/statesSmall.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "basepath": "vcmi/battleQueue/", - "images" : - [ - { "frame" : 0, "file" : "defendSmall"}, - { "frame" : 1, "file" : "waitSmall"} - ] -} diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/waitBig.png b/Mods/vcmi/Sprites/vcmi/battleQueue/waitBig.png deleted file mode 100644 index ed0b70ae6..000000000 Binary files a/Mods/vcmi/Sprites/vcmi/battleQueue/waitBig.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/waitSmall.png b/Mods/vcmi/Sprites/vcmi/battleQueue/waitSmall.png deleted file mode 100644 index da6ebed50..000000000 Binary files a/Mods/vcmi/Sprites/vcmi/battleQueue/waitSmall.png and /dev/null differ diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index b561b666f..0d1d6b65d 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -214,33 +214,5 @@ "config/rmg/symmetric/8xm12.JSON", "config/rmg/symmetric/8xm12a.JSON", "config/rmg/symmetric/8xm8.JSON" - ], - - "filesystem": - { - "CONFIG/" : - [ - {"type" : "dir", "path" : "/Config"} - ], - "DATA/" : - [ - {"type" : "dir", "path" : "/Data"} - ], - "SPRITES/": - [ - {"type" : "dir", "path" : "/Sprites"} - ], - "MAPS/": - [ - {"type" : "dir", "path" : "/Maps"} - ], - "SOUNDS/": - [ - {"type" : "dir", "path" : "/Sounds"} - ], - "VIDEO/": - [ - {"type" : "dir", "path" : "/Video"} - ] - } + ] } diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 5de831a28..e44f6ec5e 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -347,6 +347,7 @@ set(vcmiclientcommon_HEADERS widgets/CArtifactsOfHeroAltar.h widgets/CArtifactsOfHeroMarket.h widgets/CArtifactsOfHeroBackpack.h + widgets/IVideoHolder.h widgets/RadialMenu.h widgets/VideoWidget.h widgets/markets/CAltarArtifacts.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 23a73a933..1462e7977 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -403,6 +403,18 @@ void CPlayerInterface::heroKilled(const CGHeroInstance* hero) localState->erasePath(hero); } +void CPlayerInterface::townRemoved(const CGTownInstance* town) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if(town->tempOwner == playerID) + { + localState->removeOwnedTown(town); + adventureInt->onTownChanged(town); + } +} + + void CPlayerInterface::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) { EVENT_HANDLER_CALLED_BY_CLIENT; @@ -1424,6 +1436,12 @@ void CPlayerInterface::objectRemoved(const CGObjectInstance * obj, const PlayerC const CGHeroInstance * h = static_cast(obj); heroKilled(h); } + + if(obj->ID == Obj::TOWN && obj->tempOwner == playerID) + { + const CGTownInstance * t = static_cast(obj); + townRemoved(t); + } GH.fakeMouseMove(); } diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index a0f57d271..af32faf6e 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -220,6 +220,7 @@ private: }; void heroKilled(const CGHeroInstance* hero); + void townRemoved(const CGTownInstance* town); void garrisonsChanged(std::vector objs); void requestReturningToMainMenu(bool won); void acceptTurn(QueryID queryID, bool hotseatWait); //used during hot seat after your turn message is close diff --git a/client/GameChatHandler.cpp b/client/GameChatHandler.cpp index ceb9be0aa..34d5866e9 100644 --- a/client/GameChatHandler.cpp +++ b/client/GameChatHandler.cpp @@ -93,7 +93,7 @@ void GameChatHandler::onNewGameMessageReceived(PlayerColor sender, const std::st playerName = LOCPLINT->cb->getStartInfo()->playerInfos.at(sender).name; if (sender.isSpectator()) - playerName = "Spectator"; // FIXME: translate? Provide nickname somewhere? + playerName = VLC->generaltexth->translate("vcmi.lobby.login.spectator"); chatHistory.push_back({playerName, messageText, timeFormatted}); diff --git a/client/PlayerLocalState.cpp b/client/PlayerLocalState.cpp index 66d3d69ff..29e664498 100644 --- a/client/PlayerLocalState.cpp +++ b/client/PlayerLocalState.cpp @@ -396,16 +396,19 @@ void PlayerLocalState::deserialize(const JsonNode & source) } } - spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer(); - spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer(); - spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer(); - spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer(); + if (!source["spellbook"].isNull()) + { + spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer(); + spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer(); + spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer(); + spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer(); + } // append any owned heroes / towns that were not present in loaded state wanderingHeroes.insert(wanderingHeroes.end(), oldHeroes.begin(), oldHeroes.end()); ownedTowns.insert(ownedTowns.end(), oldTowns.begin(), oldTowns.end()); -//FIXME: broken, anything that is selected in here will be overwritten on NewTurn pack +//FIXME: broken, anything that is selected in here will be overwritten on PlayerStartsTurn pack // ObjectInstanceID selectedObjectID(source["currentSelection"].Integer()); // const CGObjectInstance * objectPtr = owner.cb->getObjInstance(selectedObjectID); // const CArmedInstance * armyPtr = dynamic_cast(objectPtr); diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 020f4a323..3b918de77 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -34,6 +34,7 @@ #include "../render/IImage.h" #include "../render/IRenderHandler.h" #include "../render/IScreenHandler.h" +#include "../render/AssetGenerator.h" #include "../CMT.h" #include "../PlayerLocalState.h" #include "../CPlayerInterface.h" @@ -64,6 +65,8 @@ AdventureMapInterface::AdventureMapInterface(): pos.w = GH.screenDimensions().x; pos.h = GH.screenDimensions().y; + AssetGenerator::createPaletteShiftedSprites(); + shortcuts = std::make_shared(*this); widget = std::make_shared(shortcuts); diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 121853931..d25a612a3 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -450,7 +450,7 @@ void CTownList::CTownItem::open() void CTownList::CTownItem::showTooltip() { - CRClickPopup::createAndPush(town, GH.getCursorPosition()); + CRClickPopup::createAndPush(town, pos.center()); } void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 434ec46cb..e5920ade9 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -275,7 +275,7 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac return 2; break; case PossiblePlayerBattleAction::SHOOT: - if(targetStack == nullptr || targetStack->unitSide() == stack->unitSide()) + if(targetStack == nullptr || targetStack->unitSide() == stack->unitSide() || !targetStack->alive()) return 100; //bottom priority return 4; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index b7622316c..f9dde7e68 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -1066,11 +1066,13 @@ StackQueue::StackBox::StackBox(StackQueue * owner): roundRect = std::make_shared(Rect(0, 0, 15, 18), ColorRGBA(0, 0, 0, 255), ColorRGBA(241, 216, 120, 255)); round = std::make_shared(4, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); - int icon_x = pos.w - 17; - int icon_y = pos.h - 18; + Point iconPos(pos.w - 16, pos.h - 16); - stateIcon = std::make_shared(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL"), 0, 0, icon_x, icon_y); - stateIcon->visible = false; + defendIcon = std::make_shared(ImagePath::builtin("battle/QueueDefend"), iconPos); + waitIcon = std::make_shared(ImagePath::builtin("battle/QueueWait"), iconPos); + + defendIcon->setEnabled(false); + waitIcon->setEnabled(false); } roundRect->disable(); } @@ -1106,22 +1108,13 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std:: round->setText(tmp); } - if(stateIcon) + if(!owner->embedded) { - if(unit->defended((int)turn) || (turn > 0 && unit->defended((int)turn - 1))) - { - stateIcon->setFrame(0, 0); - stateIcon->visible = true; - } - else if(unit->waited((int)turn)) - { - stateIcon->setFrame(1, 0); - stateIcon->visible = true; - } - else - { - stateIcon->visible = false; - } + bool defended = unit->defended(turn) || (turn > 0 && unit->defended(turn - 1)); + bool waited = unit->waited(turn) && !defended; + + defendIcon->setEnabled(defended); + waitIcon->setEnabled(waited); } } else @@ -1131,9 +1124,11 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std:: icon->visible = false; icon->setFrame(0); amount->setText(""); - - if(stateIcon) - stateIcon->visible = false; + if(!owner->embedded) + { + defendIcon->setEnabled(false); + waitIcon->setEnabled(false); + } } } diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index 819ed3504..9465502bc 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -260,7 +260,8 @@ class StackQueue : public CIntObject std::shared_ptr background; std::shared_ptr icon; std::shared_ptr amount; - std::shared_ptr stateIcon; + std::shared_ptr waitIcon; + std::shared_ptr defendIcon; std::shared_ptr round; std::shared_ptr roundRect; diff --git a/client/eventsSDL/InputSourceGameController.cpp b/client/eventsSDL/InputSourceGameController.cpp index 94e3ac450..5d4ff29ec 100644 --- a/client/eventsSDL/InputSourceGameController.cpp +++ b/client/eventsSDL/InputSourceGameController.cpp @@ -18,6 +18,7 @@ #include "../gui/CursorHandler.h" #include "../gui/EventDispatcher.h" #include "../gui/ShortcutHandler.h" +#include "../render/IScreenHandler.h" #include "../../lib/CConfigHandler.h" @@ -198,9 +199,10 @@ void InputSourceGameController::tryToConvertCursor() assert(CCS->curh); if(CCS->curh->getShowType() == Cursor::ShowType::HARDWARE) { + int scalingFactor = GH.screenHandler().getScalingFactor(); const Point & cursorPosition = GH.getCursorPosition(); CCS->curh->changeCursor(Cursor::ShowType::SOFTWARE); - CCS->curh->cursorMove(cursorPosition.x, cursorPosition.y); + CCS->curh->cursorMove(cursorPosition.x * scalingFactor, cursorPosition.y * scalingFactor); GH.input().setCursorPosition(cursorPosition); } } @@ -225,12 +227,13 @@ void InputSourceGameController::doCursorMove(int deltaX, int deltaY) return; const Point & screenSize = GH.screenDimensions(); const Point & cursorPosition = GH.getCursorPosition(); + int scalingFactor = GH.screenHandler().getScalingFactor(); int newX = std::min(std::max(cursorPosition.x + deltaX, 0), screenSize.x); int newY = std::min(std::max(cursorPosition.y + deltaY, 0), screenSize.y); Point targetPosition{newX, newY}; GH.input().setCursorPosition(targetPosition); if(CCS && CCS->curh) - CCS->curh->cursorMove(GH.getCursorPosition().x, GH.getCursorPosition().y); + CCS->curh->cursorMove(GH.getCursorPosition().x * scalingFactor, GH.getCursorPosition().y * scalingFactor); } int InputSourceGameController::getMoveDis(float planDis) diff --git a/client/globalLobby/GlobalLobbyRoomWindow.cpp b/client/globalLobby/GlobalLobbyRoomWindow.cpp index f744dca2d..943737b72 100644 --- a/client/globalLobby/GlobalLobbyRoomWindow.cpp +++ b/client/globalLobby/GlobalLobbyRoomWindow.cpp @@ -27,7 +27,7 @@ #include "../widgets/ObjectLists.h" #include "../../lib/modding/CModHandler.h" -#include "../../lib/modding/CModInfo.h" +#include "../../lib/modding/ModDescription.h" #include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/texts/MetaString.h" @@ -128,14 +128,14 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s GlobalLobbyRoomModInfo modInfo; modInfo.status = modEntry.second; if (modEntry.second == ModVerificationStatus::EXCESSIVE) - modInfo.version = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().version.toString(); + modInfo.version = CGI->modh->getModInfo(modEntry.first).getVersion().toString(); else modInfo.version = roomDescription.modList.at(modEntry.first).version.toString(); if (modEntry.second == ModVerificationStatus::NOT_INSTALLED) modInfo.modName = roomDescription.modList.at(modEntry.first).name; else - modInfo.modName = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().name; + modInfo.modName = CGI->modh->getModInfo(modEntry.first).getName(); modVerificationList.push_back(modInfo); } diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index bf25bedbc..9b13ece93 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -263,7 +263,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) humanCountAllowed = tmpl->getHumanPlayers().getNumbers(); // Unused now? } - si8 playerLimit = opts->getMaxPlayersCount(); + si8 playerLimit = opts->getPlayerLimit(); si8 humanOrCpuPlayerCount = opts->getHumanOrCpuPlayerCount(); si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount(); @@ -469,6 +469,8 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): variables["totalPlayers"].Integer() = totalPlayers; pos.w = variables["windowSize"]["x"].Integer() + totalPlayers * variables["cellMargin"]["x"].Integer(); + auto widthExtend = std::max(pos.w, 220) - pos.w; // too small for buttons + pos.w += widthExtend; pos.h = variables["windowSize"]["y"].Integer() + totalPlayers * variables["cellMargin"]["y"].Integer(); variables["backgroundRect"]["x"].Integer() = 0; variables["backgroundRect"]["y"].Integer() = 0; @@ -553,7 +555,7 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): for(int teamId = 0; teamId < totalPlayers; ++teamId) { - variables["point"]["x"].Integer() = variables["cellOffset"]["x"].Integer() + plId * variables["cellMargin"]["x"].Integer(); + variables["point"]["x"].Integer() = variables["cellOffset"]["x"].Integer() + plId * variables["cellMargin"]["x"].Integer() + (widthExtend / 2); variables["point"]["y"].Integer() = variables["cellOffset"]["y"].Integer() + teamId * variables["cellMargin"]["y"].Integer(); auto button = buildWidget(variables["button"]); players.back()->addToggle(teamId, std::dynamic_pointer_cast(button)); diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 953f2ce2f..ab985497a 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -205,7 +205,7 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc } else { - videoPlayer = std::make_shared(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, [this](){close();}); + videoPlayer = std::make_shared(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, this); CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); } @@ -216,6 +216,11 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc } } +void CHighScoreInputScreen::onVideoPlaybackFinished() +{ + close(); +} + int CHighScoreInputScreen::addEntry(std::string text) { std::vector baseNode = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"].Vector(); diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index dbb9aa60c..e0c3b5c69 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -8,6 +8,8 @@ * */ #pragma once + +#include "../widgets/IVideoHolder.h" #include "../windows/CWindowObject.h" #include "../../lib/gameState/HighScore.h" #include "../../lib/gameState/GameStatistics.h" @@ -69,7 +71,7 @@ public: CHighScoreInput(std::string playerName, std::function readyCB); }; -class CHighScoreInputScreen : public CWindowObject +class CHighScoreInputScreen : public CWindowObject, public IVideoHolder { std::vector> texts; std::shared_ptr input; @@ -82,6 +84,8 @@ class CHighScoreInputScreen : public CWindowObject bool won; HighScoreCalculation calc; StatisticDataSet stat; + + void onVideoPlaybackFinished() override; public: CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic); diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 565d8bb9a..f3702101c 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -362,17 +362,6 @@ void CMainMenu::update() menu->switchToTab(menu->getActiveTab()); } - static bool warnedAboutModDependencies = false; - - if (!warnedAboutModDependencies) - { - warnedAboutModDependencies = true; - auto errorMessages = CGI->modh->getModLoadErrors(); - - if (!errorMessages.empty()) - CInfoWindow::showInfoDialog(errorMessages, std::vector>(), PlayerColor(1)); - } - // Handles mouse and key input GH.handleEvents(); GH.windows().simpleRedraw(); diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index 9b56ccd9d..1a2ae9089 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -121,21 +121,31 @@ void MapTileStorage::load(size_t index, const AnimationPath & filename, EImageBl terrainAnimations[3]->horizontalFlip(); } -std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIndex, size_t imageIndex) +std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIndex, size_t imageIndex, size_t groupIndex) { const auto & animation = animations[fileIndex][rotationIndex]; if (animation) - return animation->getImage(imageIndex); + return animation->getImage(imageIndex, groupIndex); else return nullptr; } +int MapTileStorage::groupCount(size_t fileIndex, size_t rotationIndex, size_t imageIndex) +{ + const auto & animation = animations[fileIndex][rotationIndex]; + if (animation) + for(int i = 0;; i++) + if(!animation->getImage(imageIndex, i, false)) + return i; + return 1; +} + MapRendererTerrain::MapRendererTerrain() : storage(VLC->terrainTypeHandler->objects.size()) { logGlobal->debug("Loading map terrains"); for(const auto & terrain : VLC->terrainTypeHandler->objects) - storage.load(terrain->getIndex(), terrain->tilesFilename, EImageBlitMode::OPAQUE); + storage.load(terrain->getIndex(), AnimationPath::builtin(terrain->tilesFilename.getName() + (terrain->paletteAnimation.size() ? "_Shifted": "")), EImageBlitMode::OPAQUE); logGlobal->debug("Done loading map terrains"); } @@ -147,7 +157,8 @@ void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & targ int32_t imageIndex = mapTile.terView; int32_t rotationIndex = mapTile.extTileFlags % 4; - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); + int groupCount = storage.groupCount(terrainIndex, rotationIndex, imageIndex); + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex, groupCount > 1 ? context.terrainImageIndex(groupCount) : 0); assert(image); if (!image) @@ -156,9 +167,6 @@ void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & targ return; } - for( auto const & element : mapTile.getTerrain()->paletteAnimation) - image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); - target.draw(image, Point(0, 0)); } @@ -176,7 +184,7 @@ MapRendererRiver::MapRendererRiver() { logGlobal->debug("Loading map rivers"); for(const auto & river : VLC->riverTypeHandler->objects) - storage.load(river->getIndex(), river->tilesFilename, EImageBlitMode::COLORKEY); + storage.load(river->getIndex(), AnimationPath::builtin(river->tilesFilename.getName() + (river->paletteAnimation.size() ? "_Shifted": "")), EImageBlitMode::COLORKEY); logGlobal->debug("Done loading map rivers"); } @@ -191,10 +199,8 @@ void MapRendererRiver::renderTile(IMapRendererContext & context, Canvas & target int32_t imageIndex = mapTile.riverDir; int32_t rotationIndex = (mapTile.extTileFlags >> 2) % 4; - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); - - for( auto const & element : mapTile.getRiver()->paletteAnimation) - image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); + int groupCount = storage.groupCount(terrainIndex, rotationIndex, imageIndex); + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex, groupCount > 1 ? context.terrainImageIndex(groupCount) : 0); target.draw(image, Point(0, 0)); } diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index e248f8cec..085846c63 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -33,7 +33,8 @@ class MapTileStorage public: explicit MapTileStorage(size_t capacity); void load(size_t index, const AnimationPath & filename, EImageBlitMode blitMode); - std::shared_ptr find(size_t fileIndex, size_t rotationIndex, size_t imageIndex); + std::shared_ptr find(size_t fileIndex, size_t rotationIndex, size_t imageIndex, size_t groupIndex = 0); + int groupCount(size_t fileIndex, size_t rotationIndex, size_t imageIndex); }; class MapRendererTerrain diff --git a/client/mapView/MapRendererContext.cpp b/client/mapView/MapRendererContext.cpp index d6644b394..c12c1ccc4 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -548,7 +548,10 @@ size_t MapRendererSpellViewContext::overlayImageIndex(const int3 & coordinates) return iconIndex; } - return MapRendererWorldViewContext::overlayImageIndex(coordinates); + if (MapRendererBaseContext::isVisible(coordinates)) + return MapRendererWorldViewContext::overlayImageIndex(coordinates); + else + return std::numeric_limits::max(); } MapRendererPuzzleMapContext::MapRendererPuzzleMapContext(const MapRendererContextState & viewState) diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h index 619591c9b..6497f20a2 100644 --- a/client/media/CEmptyVideoPlayer.h +++ b/client/media/CEmptyVideoPlayer.h @@ -14,10 +14,6 @@ class CEmptyVideoPlayer final : public IVideoPlayer { public: - void playSpellbookAnimation(const VideoPath & name, const Point & position) override - { - } - /// Load video from specified path std::unique_ptr open(const VideoPath & name, float scaleFactor) override { diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index a2a0ef939..7853c9f1b 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -637,68 +637,6 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide return dat; } -bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool stopOnKey) -{ - CVideoInstance instance; - - auto extractedAudio = getAudio(name); - int audioHandle = CCS->soundh->playSound(extractedAudio); - - if (!instance.openInput(name)) - return true; - - instance.openVideo(); - instance.prepareOutput(1, true); - - auto lastTimePoint = boost::chrono::steady_clock::now(); - - while(instance.loadNextFrame()) - { - if(stopOnKey) - { - GH.input().fetchEvents(); - if(GH.input().ignoreEventsUntilInput()) - { - CCS->soundh->stopSound(audioHandle); - return false; - } - } - - SDL_Rect rect; - rect.x = position.x; - rect.y = position.y; - rect.w = instance.dimensions.x; - rect.h = instance.dimensions.y; - - SDL_RenderFillRect(mainRenderer, &rect); - - if(instance.textureYUV) - SDL_RenderCopy(mainRenderer, instance.textureYUV, nullptr, &rect); - else - SDL_RenderCopy(mainRenderer, instance.textureRGB, nullptr, &rect); - - SDL_RenderPresent(mainRenderer); - - // Framerate delay - double targetFrameTimeSeconds = instance.getCurrentFrameDuration(); - auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * targetFrameTimeSeconds)); - - auto timePointAfterPresent = boost::chrono::steady_clock::now(); - auto timeSpentBusy = boost::chrono::duration_cast(timePointAfterPresent - lastTimePoint); - - if(targetFrameTime > timeSpentBusy) - boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); - - lastTimePoint = boost::chrono::steady_clock::now(); - } - return true; -} - -void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & position) -{ - openAndPlayVideoImpl(name, position * GH.screenHandler().getScalingFactor(), false, false); -} - std::unique_ptr CVideoPlayer::open(const VideoPath & name, float scaleFactor) { auto result = std::make_unique(); diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index 63e4a6176..591f83b74 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -103,11 +103,9 @@ public: class CVideoPlayer final : public IVideoPlayer { - bool openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool stopOnKey); void openVideoFile(CVideoInstance & state, const VideoPath & fname); public: - void playSpellbookAnimation(const VideoPath & name, const Point & position) final; std::unique_ptr open(const VideoPath & name, float scaleFactor) final; std::pair, si64> getAudio(const VideoPath & videoToOpen) final; }; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index 3f2784c16..35f385bc1 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -45,9 +45,6 @@ public: class IVideoPlayer : boost::noncopyable { public: - /// Plays video on top of the screen, returns only after playback is over - virtual void playSpellbookAnimation(const VideoPath & name, const Point & position) = 0; - /// Load video from specified path. Returns nullptr on failure virtual std::unique_ptr open(const VideoPath & name, float scaleFactor) = 0; diff --git a/client/render/AssetGenerator.cpp b/client/render/AssetGenerator.cpp index 5aeb5f734..a1e4befe7 100644 --- a/client/render/AssetGenerator.cpp +++ b/client/render/AssetGenerator.cpp @@ -16,12 +16,16 @@ #include "../render/Canvas.h" #include "../render/ColorFilter.h" #include "../render/IRenderHandler.h" +#include "../render/CAnimation.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/GameSettings.h" #include "../lib/IGameSettings.h" #include "../lib/json/JsonNode.h" #include "../lib/VCMI_Lib.h" +#include "../lib/RiverHandler.h" +#include "../lib/RoadHandler.h" +#include "../lib/TerrainHandler.h" void AssetGenerator::generateAll() { @@ -32,6 +36,7 @@ void AssetGenerator::generateAll() createCombatUnitNumberWindow(); createCampaignBackground(); createChroniclesCampaignImages(); + createPaletteShiftedSprites(); } void AssetGenerator::createAdventureOptionsCleanBackground() @@ -326,3 +331,106 @@ void AssetGenerator::createChroniclesCampaignImages() image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); } } + +void AssetGenerator::createPaletteShiftedSprites() +{ + std::vector tiles; + std::vector>> paletteAnimations; + for(auto entity : VLC->terrainTypeHandler->objects) + { + if(entity->paletteAnimation.size()) + { + tiles.push_back(entity->tilesFilename.getName()); + std::vector> tmpAnim; + for(auto & animEntity : entity->paletteAnimation) + tmpAnim.push_back(animEntity); + paletteAnimations.push_back(tmpAnim); + } + } + for(auto entity : VLC->riverTypeHandler->objects) + { + if(entity->paletteAnimation.size()) + { + tiles.push_back(entity->tilesFilename.getName()); + std::vector> tmpAnim; + for(auto & animEntity : entity->paletteAnimation) + tmpAnim.push_back(animEntity); + paletteAnimations.push_back(tmpAnim); + } + } + + for(int i = 0; i < tiles.size(); i++) + { + auto sprite = tiles[i]; + + JsonNode config; + config["basepath"].String() = sprite + "_Shifted/"; + config["images"].Vector(); + + auto filename = AnimationPath::builtin(sprite).addPrefix("SPRITES/"); + auto filenameNew = AnimationPath::builtin(sprite + "_Shifted").addPrefix("SPRITES/"); + + if(CResourceHandler::get()->existsResource(ResourcePath(filenameNew.getName(), EResType::JSON))) // overridden by mod, no generation + return; + + auto anim = GH.renderHandler().loadAnimation(filename, EImageBlitMode::COLORKEY); + for(int j = 0; j < anim->size(); j++) + { + int maxLen = 1; + for(int k = 0; k < paletteAnimations[i].size(); k++) + { + auto element = paletteAnimations[i][k]; + if(std::holds_alternative(element)) + maxLen = std::lcm(maxLen, std::get(element).length); + else + maxLen = std::lcm(maxLen, std::get(element).length); + } + for(int l = 0; l < maxLen; l++) + { + std::string spriteName = sprite + boost::str(boost::format("%02d") % j) + "_" + std::to_string(l) + ".png"; + std::string filenameNewImg = "sprites/" + sprite + "_Shifted" + "/" + spriteName; + ResourcePath savePath(filenameNewImg, EResType::IMAGE); + + if(!CResourceHandler::get("local")->createResource(filenameNewImg)) + return; + + auto imgLoc = anim->getImageLocator(j, 0); + imgLoc.scalingFactor = 1; + auto img = GH.renderHandler().loadImage(imgLoc, EImageBlitMode::COLORKEY); + for(int k = 0; k < paletteAnimations[i].size(); k++) + { + auto element = paletteAnimations[i][k]; + if(std::holds_alternative(element)) + { + auto tmp = std::get(element); + img->shiftPalette(tmp.start, tmp.length, l % tmp.length); + } + else + { + auto tmp = std::get(element); + img->shiftPalette(tmp.start, tmp.length, l % tmp.length); + } + } + + Canvas canvas = Canvas(Point(32, 32), CanvasScalingPolicy::IGNORE); + canvas.draw(img, Point((32 - img->dimensions().x) / 2, (32 - img->dimensions().y) / 2)); + std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); + image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); + + JsonNode node(JsonMap{ + { "group", JsonNode(l) }, + { "frame", JsonNode(j) }, + { "file", JsonNode(spriteName) } + }); + config["images"].Vector().push_back(node); + } + } + + ResourcePath savePath(filenameNew.getOriginalName(), EResType::JSON); + if(!CResourceHandler::get("local")->createResource(filenameNew.getOriginalName() + ".json")) + return; + + std::fstream file(CResourceHandler::get("local")->getResourceName(savePath)->c_str(), std::ofstream::out | std::ofstream::trunc); + file << config.toString(); + } +} diff --git a/client/render/AssetGenerator.h b/client/render/AssetGenerator.h index 2eb73a886..8ca08aae6 100644 --- a/client/render/AssetGenerator.h +++ b/client/render/AssetGenerator.h @@ -23,4 +23,5 @@ public: static void createCombatUnitNumberWindow(); static void createCampaignBackground(); static void createChroniclesCampaignImages(); + static void createPaletteShiftedSprites(); }; diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index eeafff200..50276cf5e 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -18,11 +18,12 @@ #include "../../lib/filesystem/Filesystem.h" #include "../../lib/json/JsonUtils.h" -bool CAnimation::loadFrame(size_t frame, size_t group) +bool CAnimation::loadFrame(size_t frame, size_t group, bool verbose) { if(size(group) <= frame) { - printError(frame, group, "LoadFrame"); + if(verbose) + printError(frame, group, "LoadFrame"); return false; } @@ -119,7 +120,7 @@ void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFra std::shared_ptr CAnimation::getImage(size_t frame, size_t group, bool verbose) { - if (!loadFrame(frame, group)) + if (!loadFrame(frame, group, verbose)) return nullptr; return getImageImpl(frame, group, verbose); } diff --git a/client/render/CAnimation.h b/client/render/CAnimation.h index 75c8d1566..6b9e66272 100644 --- a/client/render/CAnimation.h +++ b/client/render/CAnimation.h @@ -41,7 +41,7 @@ private: PlayerColor player = PlayerColor::CANNOT_DETERMINE; //loader, will be called by load(), require opened def file for loading from it. Returns true if image is loaded - bool loadFrame(size_t frame, size_t group); + bool loadFrame(size_t frame, size_t group, bool verbose = true); //unloadFrame, returns true if image has been unloaded ( either deleted or decreased refCount) bool unloadFrame(size_t frame, size_t group); @@ -50,8 +50,6 @@ private: void printError(size_t frame, size_t group, std::string type) const; std::shared_ptr getImageImpl(size_t frame, size_t group=0, bool verbose=true); - - ImageLocator getImageLocator(size_t frame, size_t group) const; public: CAnimation(const AnimationPath & Name, std::map > layout, EImageBlitMode mode); ~CAnimation(); @@ -74,5 +72,7 @@ public: void playerColored(PlayerColor player); void createFlippedGroup(const size_t sourceGroup, const size_t targetGroup); + + ImageLocator getImageLocator(size_t frame, size_t group) const; }; diff --git a/client/render/ImageLocator.cpp b/client/render/ImageLocator.cpp index aefbc6d30..515e767a0 100644 --- a/client/render/ImageLocator.cpp +++ b/client/render/ImageLocator.cpp @@ -15,15 +15,17 @@ #include "../../lib/json/JsonNode.h" - ImageLocator::ImageLocator(const JsonNode & config) - : image(ImagePath::fromJson(config["file"])) - , defFile(AnimationPath::fromJson(config["defFile"])) - , defFrame(config["defFrame"].Integer()) + : defFrame(config["defFrame"].Integer()) , defGroup(config["defGroup"].Integer()) , verticalFlip(config["verticalFlip"].Bool()) , horizontalFlip(config["horizontalFlip"].Bool()) { + if(!config["file"].isNull()) + image = ImagePath::fromJson(config["file"]); + + if(!config["defFile"].isNull()) + defFile = AnimationPath::fromJson(config["defFile"]); } ImageLocator::ImageLocator(const ImagePath & path) diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index ddff8347e..94faf6502 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -118,6 +118,12 @@ size_t CTrueTypeFont::getStringWidthScaled(const std::string & text) const { int width; TTF_SizeUTF8(font.get(), text.c_str(), &width, nullptr); + + if (outline) + width += getScalingFactor(); + if (dropShadow || outline) + width += getScalingFactor(); + return width; } diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index ccfd5bec8..0447c9786 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -55,7 +55,7 @@ std::shared_ptr RenderHandler::getAnimationFile(const AnimationPath & return result; } -std::optional RenderHandler::getPathForScaleFactor(ResourcePath path, std::string factor) +std::optional RenderHandler::getPathForScaleFactor(const ResourcePath & path, const std::string & factor) { if(path.getType() == EResType::IMAGE) { @@ -80,7 +80,7 @@ std::optional RenderHandler::getPathForScaleFactor(ResourcePath pa return std::nullopt; } -std::pair RenderHandler::getScalePath(ResourcePath p) +std::pair RenderHandler::getScalePath(const ResourcePath & p) { auto path = p; int scaleFactor = 1; @@ -142,7 +142,13 @@ void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & c JsonNode toAdd = node; JsonUtils::inherit(toAdd, base); - toAdd["file"].String() = basepath + node["file"].String(); + + if (toAdd.Struct().count("file")) + toAdd["file"].String() = basepath + node["file"].String(); + + if (toAdd.Struct().count("defFile")) + toAdd["defFile"].String() = basepath + node["defFile"].String(); + source[group][frame] = ImageLocator(toAdd); } } diff --git a/client/renderSDL/RenderHandler.h b/client/renderSDL/RenderHandler.h index d7b028762..43df617a1 100644 --- a/client/renderSDL/RenderHandler.h +++ b/client/renderSDL/RenderHandler.h @@ -29,8 +29,8 @@ class RenderHandler : public IRenderHandler std::map> fonts; std::shared_ptr getAnimationFile(const AnimationPath & path); - std::optional getPathForScaleFactor(ResourcePath path, std::string factor); - std::pair getScalePath(ResourcePath p); + std::optional getPathForScaleFactor(const ResourcePath & path, const std::string & factor); + std::pair getScalePath(const ResourcePath & p); AnimationLayoutMap & getAnimationLayout(const AnimationPath & path); void initFromJson(AnimationLayoutMap & layout, const JsonNode & config); diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 449b3bbfe..84d37e7ee 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -283,7 +283,10 @@ std::shared_ptr SDLImageShared::scaleInteger(int factor, SDL if (factor <= 0) throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor)); - if (palette && surf && surf->format->palette) + if (!surf) + return shared_from_this(); + + if (palette && surf->format->palette) SDL_SetSurfacePalette(surf, palette); SDL_Surface * scaled = nullptr; @@ -306,7 +309,7 @@ std::shared_ptr SDLImageShared::scaleInteger(int factor, SDL // erase our own reference SDL_FreeSurface(scaled); - if (surf && surf->format->palette) + if (surf->format->palette) SDL_SetSurfacePalette(surf, originalPalette); return ret; @@ -314,8 +317,8 @@ std::shared_ptr SDLImageShared::scaleInteger(int factor, SDL std::shared_ptr SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const { - float scaleX = float(size.x) / fullSize.x; - float scaleY = float(size.y) / fullSize.y; + float scaleX = static_cast(size.x) / fullSize.x; + float scaleY = static_cast(size.y) / fullSize.y; if (palette && surf->format->palette) SDL_SetSurfacePalette(surf, palette); diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index 2284e3764..9725b628b 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -49,7 +49,8 @@ void ButtonBase::update() // hero movement speed buttons: only three frames: normal, pressed and blocked/highlighted if (state == EButtonState::HIGHLIGHTED && image->size() < 4) image->setFrame(image->size()-1); - image->setFrame(stateToIndex[vstd::to_underlying(state)]); + else + image->setFrame(stateToIndex[vstd::to_underlying(state)]); } if (isActive()) diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index 7113f114b..36c0690f4 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -140,6 +140,8 @@ void CArtifactsOfHeroBase::gestureArtPlace(CComponentHolder & artPlace, const Po void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero) { curHero = hero; + if (!hero) + return; for(auto slot : artWorn) { diff --git a/client/widgets/CArtifactsOfHeroMain.cpp b/client/widgets/CArtifactsOfHeroMain.cpp index 9ca0ecc23..34458351e 100644 --- a/client/widgets/CArtifactsOfHeroMain.cpp +++ b/client/widgets/CArtifactsOfHeroMain.cpp @@ -28,7 +28,8 @@ CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position) CArtifactsOfHeroMain::~CArtifactsOfHeroMain() { - CArtifactsOfHeroBase::putBackPickedArtifact(); + if(curHero) + CArtifactsOfHeroBase::putBackPickedArtifact(); } void CArtifactsOfHeroMain::keyPressed(EShortcut key) diff --git a/client/widgets/CExchangeController.cpp b/client/widgets/CExchangeController.cpp index 0b2c25683..eb640a46c 100644 --- a/client/widgets/CExchangeController.cpp +++ b/client/widgets/CExchangeController.cpp @@ -79,6 +79,10 @@ void CExchangeController::moveArmy(bool leftToRight, std::optional heldS }); heldSlot = weakestSlot->first; } + + if (source->getCreature(heldSlot.value()) == nullptr) + return; + LOCPLINT->cb->bulkMoveArmy(source->id, target->id, heldSlot.value()); } diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index c859f0d83..78a809e72 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -370,6 +370,9 @@ void CGarrisonSlot::gesture(bool on, const Point & initialPosition, const Point if (!settings["input"]["radialWheelGarrisonSwipe"].Bool()) return; + if(GH.windows().topWindow()->isPopupWindow()) + return; + const auto * otherArmy = upg == EGarrisonType::UPPER ? owner->lowerArmy() : owner->upperArmy(); bool stackExists = myStack != nullptr; diff --git a/client/widgets/IVideoHolder.h b/client/widgets/IVideoHolder.h new file mode 100644 index 000000000..dbe03f2d2 --- /dev/null +++ b/client/widgets/IVideoHolder.h @@ -0,0 +1,17 @@ +/* + * IVideoHolder.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +class IVideoHolder +{ +public: + virtual ~IVideoHolder() = default; + virtual void onVideoPlaybackFinished() = 0; +}; diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp index 35fe4adcb..cdde58cde 100644 --- a/client/widgets/VideoWidget.cpp +++ b/client/widgets/VideoWidget.cpp @@ -10,12 +10,14 @@ #include "StdInc.h" #include "VideoWidget.h" #include "TextControls.h" +#include "IVideoHolder.h" #include "../CGameInfo.h" #include "../gui/CGuiHandler.h" #include "../media/ISoundPlayer.h" #include "../media/IVideoPlayer.h" #include "../render/Canvas.h" +#include "../render/IScreenHandler.h" #include "../../lib/filesystem/Filesystem.h" @@ -45,7 +47,26 @@ void VideoWidgetBase::playVideo(const VideoPath & fileToPlay) else if(CResourceHandler::get()->existsResource(subTitlePathVideoDir)) subTitleData = JsonNode(subTitlePathVideoDir); - videoInstance = CCS->videoh->open(fileToPlay, scaleFactor); + float preScaleFactor = 1; + VideoPath videoFile = fileToPlay; + if(GH.screenHandler().getScalingFactor() > 1) + { + std::vector factorsToCheck = {GH.screenHandler().getScalingFactor(), 4, 3, 2}; + for(auto factorToCheck : factorsToCheck) + { + std::string name = boost::algorithm::to_upper_copy(videoFile.getName()); + boost::replace_all(name, "VIDEO/", std::string("VIDEO") + std::to_string(factorToCheck) + std::string("X/")); + auto p = VideoPath::builtin(name).addPrefix("VIDEO" + std::to_string(factorToCheck) + "X/"); + if(CResourceHandler::get()->existsResource(p)) + { + preScaleFactor = 1.0 / static_cast(factorToCheck); + videoFile = p; + break; + } + } + } + + videoInstance = CCS->videoh->open(videoFile, scaleFactor * preScaleFactor); if (videoInstance) { pos.w = videoInstance->size().x; @@ -152,15 +173,17 @@ void VideoWidgetBase::tick(uint32_t msPassed) { videoInstance->tick(msPassed); + if(subTitle) + subTitle->setText(getSubTitleLine(videoInstance->timeStamp())); + if(videoInstance->videoEnded()) { videoInstance.reset(); stopAudio(); onPlaybackFinished(); + // WARNING: onPlaybackFinished call may destoy `this`. Make sure that this is the very last operation in this method! } } - if(subTitle && videoInstance) - subTitle->setText(getSubTitleLine(videoInstance->timeStamp())); } VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio) @@ -180,19 +203,19 @@ void VideoWidget::onPlaybackFinished() playVideo(loopedVideo); } -VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function & callback) +VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, IVideoHolder * owner) : VideoWidgetBase(position, video, playAudio) - , callback(callback) + , owner(owner) { } -VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, const std::function & callback) +VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, IVideoHolder * owner) : VideoWidgetBase(position, video, playAudio, scaleFactor) - , callback(callback) + , owner(owner) { } void VideoWidgetOnce::onPlaybackFinished() { - callback(); + owner->onVideoPlaybackFinished(); } diff --git a/client/widgets/VideoWidget.h b/client/widgets/VideoWidget.h index 4e2672d68..edccf3650 100644 --- a/client/widgets/VideoWidget.h +++ b/client/widgets/VideoWidget.h @@ -14,6 +14,7 @@ #include "../lib/filesystem/ResourcePath.h" #include "../lib/json/JsonNode.h" +class IVideoHolder; class IVideoInstance; class CMultiLineLabel; @@ -64,10 +65,10 @@ public: class VideoWidgetOnce final: public VideoWidgetBase { - std::function callback; + IVideoHolder * owner; void onPlaybackFinished() final; public: - VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function & callback); - VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, const std::function & callback); + VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, IVideoHolder * owner); + VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, IVideoHolder * owner); }; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index eba40eedc..3d917712e 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -456,7 +456,7 @@ void CHeroGSlot::showPopupWindow(const Point & cursorPosition) { if(hero) { - GH.windows().createAndPushWindow(Point(pos.x + 175, pos.y + 100), hero); + GH.windows().createAndPushWindow(pos.center(), hero); } } diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 635ac17fc..1ef1d08bb 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -279,7 +279,7 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li std::string t = bonusNames.count(bi.bonusSource) ? bonusNames[bi.bonusSource] : CGI->generaltexth->translate("vcmi.bonusSource.other"); int maxLen = 50; EFonts f = FONT_TINY; - Point pText = p + Point(3, 40); + Point pText = p + Point(4, 38); // 1px Black border bonusSource[leftRight].push_back(std::make_shared(pText.x - 1, pText.y, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen)); diff --git a/client/windows/CExchangeWindow.cpp b/client/windows/CExchangeWindow.cpp index 35c5d8c7f..e97f50412 100644 --- a/client/windows/CExchangeWindow.cpp +++ b/client/windows/CExchangeWindow.cpp @@ -248,7 +248,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, Point(484 + 35 * i, 154), AnimationPath::builtin("quick-exchange/unitLeft.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")), - std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i)))); + [this, i]() { creatureArrowButtonCallback(false, SlotID(i)); })); moveUnitFromRightToLeftButtons.back()->block(leftHeroBlock); moveUnitFromLeftToRightButtons.push_back( @@ -256,7 +256,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, Point(66 + 35 * i, 154), AnimationPath::builtin("quick-exchange/unitRight.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")), - std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i)))); + [this, i]() { creatureArrowButtonCallback(true, SlotID(i)); })); moveUnitFromLeftToRightButtons.back()->block(rightHeroBlock); } } @@ -264,6 +264,14 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, CExchangeWindow::update(); } +void CExchangeWindow::creatureArrowButtonCallback(bool leftToRight, SlotID slotId) +{ + if (GH.isKeyboardAltDown()) + controller.moveArmy(leftToRight, slotId); + else + controller.moveStack(leftToRight, slotId); +} + void CExchangeWindow::moveArtifactsCallback(bool leftToRight) { bool moveEquipped = !GH.isKeyboardShiftDown(); diff --git a/client/windows/CExchangeWindow.h b/client/windows/CExchangeWindow.h index d19b27279..e1c31d3e2 100644 --- a/client/windows/CExchangeWindow.h +++ b/client/windows/CExchangeWindow.h @@ -54,6 +54,7 @@ class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public std::shared_ptr backpackButtonRight; CExchangeController controller; + void creatureArrowButtonCallback(bool leftToRight, SlotID slotID); void moveArtifactsCallback(bool leftToRight); void swapArtifactsCallback(); void moveUnitsShortcut(bool leftToRight); diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 1cd49584f..ad73c3960 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -312,6 +312,7 @@ void CHeroWindow::dismissCurrent() arts->putBackPickedArtifact(); close(); LOCPLINT->cb->dismissHero(curHero); + arts->setHero(nullptr); }, nullptr); } diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index 4a9138471..ed7f09fe3 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -118,7 +118,6 @@ std::vector CMessage::breakText(std::string text, size_t maxLineWid } else printableString.append(text.data() + currPos, symbolSize); - currPos += symbolSize; } diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 6f810a2fa..c65171a4d 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -30,6 +30,7 @@ #include "../widgets/CTextInput.h" #include "../widgets/TextControls.h" #include "../widgets/Buttons.h" +#include "../widgets/VideoWidget.h" #include "../adventureMap/AdventureMapInterface.h" #include "../render/AssetGenerator.h" @@ -395,6 +396,8 @@ void CSpellWindow::fRcornerb() void CSpellWindow::show(Canvas & to) { + if(video) + video->show(to); statusBar->show(to); } @@ -493,14 +496,22 @@ void CSpellWindow::setCurrentPage(int value) void CSpellWindow::turnPageLeft() { + OBJECT_CONSTRUCTION; if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->playSpellbookAnimation(VideoPath::builtin("PGTRNLFT.SMK"), pos.topLeft() + Point(13, 14)); + video = std::make_shared(Point(13, 14), VideoPath::builtin("PGTRNLFT.SMK"), false, this); } void CSpellWindow::turnPageRight() { + OBJECT_CONSTRUCTION; if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->playSpellbookAnimation(VideoPath::builtin("PGTRNRGH.SMK"), pos.topLeft() + Point(13, 14)); + video = std::make_shared(Point(13, 14), VideoPath::builtin("PGTRNRGH.SMK"), false, this); +} + +void CSpellWindow::onVideoPlaybackFinished() +{ + video.reset(); + redraw(); } void CSpellWindow::keyPressed(EShortcut key) diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index 29293faa4..6f5eeb719 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -10,6 +10,7 @@ #pragma once #include "CWindowObject.h" +#include "../widgets/IVideoHolder.h" VCMI_LIB_NAMESPACE_BEGIN @@ -28,9 +29,10 @@ class CSpellWindow; class CTextInput; class TransparentFilledRectangle; class CToggleButton; +class VideoWidgetOnce; /// The spell window -class CSpellWindow : public CWindowObject +class CSpellWindow : public CWindowObject, public IVideoHolder { class SpellArea : public CIntObject { @@ -86,6 +88,8 @@ class CSpellWindow : public CWindowObject std::shared_ptr showAllSpells; std::shared_ptr showAllSpellsDescription; + std::shared_ptr video; + bool isBigSpellbook; int spellsPerPage; int offL; @@ -113,6 +117,8 @@ class CSpellWindow : public CWindowObject void turnPageLeft(); void turnPageRight(); + void onVideoPlaybackFinished() override; + bool openOnBattleSpells; std::function onSpellSelect; //external processing of selected spell diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 209b03ef7..bd9cec14f 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -951,8 +951,8 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, BuildingID bu else if(auto uni = dynamic_cast(_market); uni->appearance) { titlePic = std::make_shared(uni->appearance->animationFile, 0, 0, 0, 0, CShowableAnim::CREATURE_MODE); - titleStr = uni->title; - speechStr = uni->speech; + titleStr = uni->getObjectName(); + speechStr = uni->getSpeechTranslated(); } else { @@ -1508,7 +1508,7 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, std::share itemsVisible = items; init(titleWidget_, _title, _descr, searchBoxEnabled); - list->scrollTo(initialSelection - 4); // -4 is for centering (list have 9 elements) + list->scrollTo(std::min(static_cast(initialSelection + 4), static_cast(items.size() - 1))); // 4 is for centering (list have 9 elements) } CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images, bool searchBoxEnabled) @@ -1528,7 +1528,7 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, st itemsVisible = items; init(titleWidget_, _title, _descr, searchBoxEnabled); - list->scrollTo(initialSelection - 4); // -4 is for centering (list have 9 elements) + list->scrollTo(std::min(static_cast(initialSelection + 4), static_cast(items.size() - 1))); // 4 is for centering (list have 9 elements) } void CObjectListWindow::init(std::shared_ptr titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled) @@ -1653,7 +1653,7 @@ void CObjectListWindow::keyPressed(EShortcut key) } vstd::abetween(sel, 0, itemsVisible.size()-1); - list->scrollTo(sel - 4); // -4 is for centering (list have 9 elements) + list->scrollTo(sel); changeSelection(sel); } @@ -1670,22 +1670,27 @@ VideoWindow::VideoWindow(const VideoPath & video, const ImagePath & rim, bool sh if(!rim.empty()) { setBackground(rim); - videoPlayer = std::make_shared(Point(80, 186), video, true, [this](){ exit(false); }); + videoPlayer = std::make_shared(Point(80, 186), video, true, this); pos = center(Rect(0, 0, 800, 600)); } else { blackBackground = std::make_shared(Rect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y)); - videoPlayer = std::make_shared(Point(0, 0), video, true, scaleFactor, [this](){ exit(false); }); + videoPlayer = std::make_shared(Point(0, 0), video, true, scaleFactor, this); pos = center(Rect(0, 0, videoPlayer->pos.w, videoPlayer->pos.h)); blackBackground->addBox(Point(0, 0), Point(pos.x, pos.y), Colors::BLACK); } if(backgroundAroundWindow) backgroundAroundWindow->pos.moveTo(Point(0, 0)); - } +void VideoWindow::onVideoPlaybackFinished() +{ + exit(false); +} + + void VideoWindow::exit(bool skipped) { close(); diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index f3c8a044c..74ee6d916 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -12,6 +12,7 @@ #include "CWindowObject.h" #include "../lib/ResourceSet.h" #include "../widgets/Images.h" +#include "../widgets/IVideoHolder.h" VCMI_LIB_NAMESPACE_BEGIN @@ -509,7 +510,7 @@ public: CThievesGuildWindow(const CGObjectInstance * _owner); }; -class VideoWindow : public CWindowObject +class VideoWindow : public CWindowObject, public IVideoHolder { std::shared_ptr videoPlayer; std::shared_ptr backgroundAroundWindow; @@ -517,6 +518,7 @@ class VideoWindow : public CWindowObject std::function closeCb; + void onVideoPlaybackFinished() override; void exit(bool skipped); public: VideoWindow(const VideoPath & video, const ImagePath & rim, bool showBackground, float scaleFactor, const std::function & closeCb); diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 2719c08d5..494192e4c 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -266,16 +266,6 @@ void CRClickPopupInt::mouseDraggedPopup(const Point & cursorPosition, const Poin close(); } -Point CInfoBoxPopup::toScreen(Point p) -{ - auto bounds = adventureInt->terrainAreaPixels(); - - vstd::abetween(p.x, bounds.top() + 100, bounds.bottom() - 100); - vstd::abetween(p.y, bounds.left() + 100, bounds.right() - 100); - - return p; -} - void CInfoBoxPopup::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) { if(!settings["adventure"]["rightButtonDrag"].Bool()) @@ -289,7 +279,7 @@ void CInfoBoxPopup::mouseDraggedPopup(const Point & cursorPosition, const Point CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), position) { InfoAboutTown iah; LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero? @@ -298,10 +288,12 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) tooltip = std::make_shared(Point(9, 10), iah); addUsedEvents(DRAG_POPUP); + + fitToScreen(10); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("HEROQVBK"), toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("HEROQVBK"), position) { InfoAboutHero iah; LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero? @@ -310,10 +302,12 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) tooltip = std::make_shared(Point(9, 10), iah); addUsedEvents(DRAG_POPUP); + + fitToScreen(10); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), position) { InfoAboutTown iah; LOCPLINT->cb->getTownInfo(garr, iah); @@ -322,15 +316,19 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) tooltip = std::make_shared(Point(9, 10), iah); addUsedEvents(DRAG_POPUP); + + fitToScreen(10); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature) - : CWindowObject(RCLICK_POPUP | BORDERED, ImagePath::builtin("DIBOXBCK"), toScreen(position)) + : CWindowObject(RCLICK_POPUP | BORDERED, ImagePath::builtin("DIBOXBCK"), position) { OBJECT_CONSTRUCTION; tooltip = std::make_shared(Point(9, 10), creature); addUsedEvents(DRAG_POPUP); + + fitToScreen(10); } std::shared_ptr diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index 81b661339..468088dd2 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -90,7 +90,6 @@ public: class CInfoBoxPopup : public CWindowObject { std::shared_ptr tooltip; - Point toScreen(Point pos); Point dragDistance; diff --git a/config/ai/nkai/nkai-settings.json b/config/ai/nkai/nkai-settings.json index f597be497..a4356c34a 100644 --- a/config/ai/nkai/nkai-settings.json +++ b/config/ai/nkai/nkai-settings.json @@ -1,10 +1,129 @@ { - "maxRoamingHeroes" : 8, - "maxpass" : 30, - "mainHeroTurnDistanceLimit" : 10, - "scoutHeroTurnDistanceLimit" : 5, - "maxGoldPressure" : 0.3, - "useTroopsFromGarrisons" : true, - "openMap": true, - "allowObjectGraph": true + // "maxRoamingHeroes" - AI will never recruit new heroes above this value. + // Note that AI might end up with more heroes - due to prisons or if he has large number of heroes on start + // + // "maxpass" - ??? + // + // "mainHeroTurnDistanceLimit" - AI will only run pathfinding for specified number of turns for his main hero. + // "scoutHeroTurnDistanceLimit" - AI will only run pathfinding for specified number of turns for his secondary (scout) heroes + // Limiting this will make AI faster, but may result in AI being unable to discover objects outside of this range + // + // "maxGoldPressure" - ??? + // + // "useTroopsFromGarrisons" - AI can take troops from garrisons on map. + // Note that at the moment AI will not deliberately seek out such garrisons, he can only take troops from them when passing through. + // This option is always disabled on H3 RoE campaign maps to be in line with H3 AI + // + // "openMap" - AI will use map reveal cheat if cheats are enabled and AI is not allied with human player + // This improves AI decision making, but may lead AI to deliberately targeting targets that he should not be able to see at the moment + // + // "allowObjectGraph" - if used, AI will build "cache" for pathfinder on first turn, which should make AI faster. Requires openMap. + // + // "pathfinderBucketsCount" - ??? + // "pathfinderBucketSize" - ??? + // + // "retreatThresholdRelative" - AI will consider retreating from battle only if his troops are less than specified ratio compated to enemy + // "retreatThresholdAbsolute" - AI will consider retreating from battle only if total fight value of his troops are less than specified value + // + // "maxArmyLossTarget" - AI will try keep army loss below specified target + // + // "safeAttackRatio" - TODO: figure out how exactly it affects AI decision making + // + // "useFuzzy" - allow using of fuzzy logic. TODO: better description + + + "pawn" : { + "maxRoamingHeroes" : 4, //H3 value: 3, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0, + "retreatThresholdAbsolute" : 0, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.5, + "useFuzzy" : false + }, + + "knight" : { + "maxRoamingHeroes" : 6, //H3 value: 3, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.1, + "retreatThresholdAbsolute" : 5000, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.35, + "useFuzzy" : false + }, + + "rook" : { + "maxRoamingHeroes" : 8, //H3 value: 4 + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.25, + "useFuzzy" : false + }, + + "queen" : { + "maxRoamingHeroes" : 8, //H3 value: 5 + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.25, + "useFuzzy" : false + }, + + "king" : { + "maxRoamingHeroes" : 8, //H3 value: 6 + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.25, + "useFuzzy" : false + } } \ No newline at end of file diff --git a/config/creatures/castle.json b/config/creatures/castle.json index 34f13b06b..d769f8679 100644 --- a/config/creatures/castle.json +++ b/config/creatures/castle.json @@ -57,6 +57,7 @@ "extraNames": [ "lightCrossbowman" ], "faction": "castle", "upgrades": ["marksman"], + "shots" : 12, "abilities" : { "shooter" : { @@ -86,6 +87,7 @@ "index": 3, "level": 2, "faction": "castle", + "shots" : 24, "abilities": { "shooter" : { @@ -228,6 +230,7 @@ "level": 5, "faction": "castle", "upgrades": ["zealot"], + "shots" : 12, "abilities" : { "shooter" : { @@ -257,6 +260,7 @@ "index": 9, "level": 5, "faction": "castle", + "shots" : 24, "abilities" : { "shooter" : { diff --git a/config/creatures/conflux.json b/config/creatures/conflux.json index dd0d9fc8a..78cb70ec1 100755 --- a/config/creatures/conflux.json +++ b/config/creatures/conflux.json @@ -127,6 +127,7 @@ "index": 127, "level": 2, "faction": "conflux", + "shots" : 24, "abilities": { "nonLiving" : @@ -301,6 +302,7 @@ "level": 3, "faction": "conflux", "doubleWide" : true, + "shots" : 24, "abilities": { "nonLiving" : @@ -472,11 +474,11 @@ { "type" : "NON_LIVING" }, - "canFly" : + "energizes" : { "type" : "FLYING" }, - "spellcaster": + "spellcaster" : { "type" : "SPELLCASTER", "subtype" : "spell.protectFire", diff --git a/config/creatures/dungeon.json b/config/creatures/dungeon.json index 39e6f8b67..4d7492983 100644 --- a/config/creatures/dungeon.json +++ b/config/creatures/dungeon.json @@ -138,6 +138,7 @@ "level": 3, "faction": "dungeon", "upgrades": ["evilEye"], + "shots" : 12, "abilities" : { "shooter" : @@ -179,6 +180,7 @@ "index": 75, "level": 3, "faction": "dungeon", + "shots" : 24, "abilities" : { "shooter" : @@ -221,6 +223,7 @@ "level": 4, "faction": "dungeon", "doubleWide" : true, + "shots" : 4, "abilities": { "shooter" : @@ -264,6 +267,7 @@ "level": 4, "faction": "dungeon", "doubleWide" : true, + "shots" : 8, "abilities": { "shooter" : diff --git a/config/creatures/fortress.json b/config/creatures/fortress.json index ddcafe3a6..f834f993a 100644 --- a/config/creatures/fortress.json +++ b/config/creatures/fortress.json @@ -44,6 +44,7 @@ "faction": "fortress", "upgrades": ["lizardWarrior"], "hasDoubleWeek": true, + "shots" : 12, "abilities" : { "shooter" : @@ -74,6 +75,7 @@ "index": 101, "level": 2, "faction": "fortress", + "shots" : 24, "abilities" : { "shooter" : diff --git a/config/creatures/inferno.json b/config/creatures/inferno.json index 491968747..cbea72a91 100755 --- a/config/creatures/inferno.json +++ b/config/creatures/inferno.json @@ -51,6 +51,7 @@ "faction": "inferno", "upgrades": ["magog"], "hasDoubleWeek": true, + "shots" : 12, "abilities" : { "shooter" : @@ -81,6 +82,7 @@ "index": 45, "level": 2, "faction": "inferno", + "shots" : 24, "abilities": { "shooter" : @@ -353,7 +355,7 @@ "faction": "inferno", "abilities": { - "canFly" : + "teleports" : { "type" : "FLYING", "subtype" : "movementTeleporting" @@ -413,7 +415,7 @@ "faction": "inferno", "abilities" : { - "canFly" : + "teleports" : { "type" : "FLYING", "subtype" : "movementTeleporting" diff --git a/config/creatures/necropolis.json b/config/creatures/necropolis.json index ebbb5dc0b..06a71382d 100644 --- a/config/creatures/necropolis.json +++ b/config/creatures/necropolis.json @@ -265,6 +265,7 @@ "index": 64, "level": 5, "faction": "necropolis", + "shots" : 12, "abilities": { "undead" : @@ -305,6 +306,7 @@ "index": 65, "level": 5, "faction": "necropolis", + "shots" : 24, "abilities": { "undead" : diff --git a/config/creatures/neutral.json b/config/creatures/neutral.json index a8e7db89d..0e25c5231 100644 --- a/config/creatures/neutral.json +++ b/config/creatures/neutral.json @@ -323,6 +323,7 @@ "extraNames": [ "enchanters" ], "faction": "neutral", "excludeFromRandomization" : true, + "shots" : 32, "abilities": { "shooter" : @@ -410,6 +411,7 @@ "extraNames": [ "sharpshooters" ], "faction": "neutral", "excludeFromRandomization" : true, + "shots" : 32, "abilities": { "shooter" : @@ -448,6 +450,7 @@ "index": 138, "level": 1, "faction": "neutral", + "shots" : 24, "abilities": { "shooter" : diff --git a/config/creatures/rampart.json b/config/creatures/rampart.json index 7a64a1460..3b28901d7 100644 --- a/config/creatures/rampart.json +++ b/config/creatures/rampart.json @@ -101,6 +101,7 @@ "level": 3, "faction": "rampart", "upgrades": ["grandElf"], + "shots" : 24, "abilities" : { "shooter" : @@ -131,7 +132,8 @@ "index": 19, "level": 3, "faction": "rampart", - "abilities": + "shots" : 24, + "abilities" : { "shooter" : { diff --git a/config/creatures/special.json b/config/creatures/special.json index f2457fcb4..4e528117a 100644 --- a/config/creatures/special.json +++ b/config/creatures/special.json @@ -37,6 +37,7 @@ "level": 0, "faction": "neutral", "doubleWide" : true, + "shots" : 24, "abilities" : { "siegeWeapon" : @@ -75,6 +76,7 @@ "level": 0, "faction": "neutral", "doubleWide" : true, + "shots" : 24, "abilities" : { "siegeWeapon" : @@ -163,8 +165,10 @@ "index": 149, "level": 0, "faction": "neutral", + "shots" : 99, "abilities": { + "siegeWeapon" : { "type" : "SIEGE_WEAPON" }, "shooter" : { "type" : "SHOOTER" }, "ignoreDefence" : { "type" : "ENEMY_DEFENCE_REDUCTION", "val" : 100 }, "noWallPenalty" : { "type" : "NO_WALL_PENALTY" }, diff --git a/config/creatures/stronghold.json b/config/creatures/stronghold.json index b52a3cc82..7824a2b58 100644 --- a/config/creatures/stronghold.json +++ b/config/creatures/stronghold.json @@ -92,6 +92,7 @@ "level": 3, "faction": "stronghold", "upgrades": ["orcChieftain"], + "shots" : 12, "abilities" : { "shooter" : @@ -122,6 +123,7 @@ "index": 89, "level": 3, "faction": "stronghold", + "shots" : 24, "abilities" : { "shooter" : @@ -274,6 +276,7 @@ "index": 94, "level": 6, "faction": "stronghold", + "shots" : 16, "abilities" : { "shooter" : @@ -310,6 +313,7 @@ "index": 95, "level": 6, "faction": "stronghold", + "shots" : 24, "abilities": { "shooter" : diff --git a/config/creatures/tower.json b/config/creatures/tower.json index 259b6e056..99781a907 100644 --- a/config/creatures/tower.json +++ b/config/creatures/tower.json @@ -26,6 +26,7 @@ "index": 29, "level": 1, "faction": "tower", + "shots" : 8, "abilities" : { "shooter" : @@ -178,6 +179,7 @@ "index": 34, "level": 4, "faction": "tower", + "shots" : 24, "abilities": { "shooter" : @@ -218,6 +220,7 @@ "index": 35, "level": 4, "faction": "tower", + "shots" : 24, "abilities": { "shooter" : @@ -444,6 +447,7 @@ "index": 41, "level": 7, "faction": "tower", + "shots" : 24, "abilities" : { "shooter" : diff --git a/config/gameConfig.json b/config/gameConfig.json index 0f51e2ea7..ac40a419f 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -56,6 +56,7 @@ "config/objects/lighthouse.json", "config/objects/magicSpring.json", "config/objects/magicWell.json", + "config/objects/markets.json", "config/objects/moddables.json", "config/objects/observatory.json", "config/objects/pyramid.json", @@ -487,6 +488,20 @@ // if enabled flying will work like in original game, otherwise nerf similar to HotA flying is applied "originalFlyRules" : true }, + + "resources" : { + // H3 mechanics - AI receives bonus (or malus, on easy) to his resource income + // AI will receive specified values as percentage of his weekly income + // So, "gems" : 200 will give AI player 200% of his daily income of gems over week, or, in other words, + // giving AI player 2 additional gems per week for every owned Gem Pond + "weeklyBonusesAI" : { + "pawn" : { "gold" : -175 }, + "knight": {}, + "rook" : {}, + "queen" : { "wood" : 275 , "mercury" : 100, "ore" : 275, "sulfur" : 100, "crystal" : 100, "gems" : 100, "gold" : 175}, + "king" : { "wood" : 375 , "mercury" : 200, "ore" : 375, "sulfur" : 200, "crystal" : 200, "gems" : 200, "gold" : 350} + } + }, "spells": { diff --git a/config/heroClasses.json b/config/heroClasses.json index 6fa79b64a..f4e8d2116 100644 --- a/config/heroClasses.json +++ b/config/heroClasses.json @@ -106,7 +106,7 @@ "defaultTavern" : 5, "affinity" : "might", "commander" : "medusaQueen", - "mapObject" : { "templates" : { "default" : { "animation" : "AH11_.def", "editorAnimation": "AH11_E.def" } } }, + "mapObject" : { "templates" : { "default" : { "animation" : "AH10_.def", "editorAnimation": "AH10_E.def" } } }, "animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } } }, "warlock" : @@ -116,7 +116,7 @@ "defaultTavern" : 5, "affinity" : "magic", "commander" : "medusaQueen", - "mapObject" : { "templates" : { "default" : { "animation" : "AH10_.def", "editorAnimation": "AH10_E.def" } } }, + "mapObject" : { "templates" : { "default" : { "animation" : "AH11_.def", "editorAnimation": "AH11_E.def" } } }, "animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } } }, "barbarian" : diff --git a/config/objects/generic.json b/config/objects/generic.json index 238cfd848..3be342df6 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -18,115 +18,6 @@ } }, - "altarOfSacrifice" : { - "index" :2, - "handler" : "market", - "base" : { - "sounds" : { - "visit" : ["MYSTERY"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - "zoneLimit" : 1, - "value" : 100, - "rarity" : 20 - }, - "modes" : ["creature-experience", "artifact-experience"] - } - } - }, - "tradingPost" : { - "index" :221, - "handler" : "market", - "base" : { - "sounds" : { - "ambient" : ["LOOPMARK"], - "visit" : ["STORE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - "zoneLimit" : 1, - "value" : 100, - "rarity" : 100 - }, - "modes" : ["resource-resource", "resource-player"], - "efficiency" : 5, - "title" : "core.genrltxt.159" - } - } - }, - "tradingPostDUPLICATE" : { - "index" :99, - "handler" : "market", - "base" : { - "sounds" : { - "ambient" : ["LOOPMARK"], - "visit" : ["STORE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - "zoneLimit" : 1, - "value" : 100, - "rarity" : 100 - }, - "modes" : ["resource-resource", "resource-player"], - "efficiency" : 5, - "title" : "core.genrltxt.159" - } - } - }, - "freelancersGuild" : { - "index" :213, - "handler" : "market", - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - "zoneLimit" : 1, - "value" : 100, - "rarity" : 100 - }, - "modes" : ["creature-resource"] - } - } - }, - - "blackMarket" : { - "index" :7, - "handler" : "market", - "base" : { - "sounds" : { - "ambient" : ["LOOPMARK"], - "visit" : ["MYSTERY"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 8000, - "rmg" : { - "value" : 8000, - "rarity" : 20 - }, - "modes" : ["resource-artifact"], - "title" : "core.genrltxt.349" - } - } - }, - "pandoraBox" : { "index" :6, "handler" : "pandora", @@ -393,35 +284,6 @@ } } }, - "university" : { - "index" :104, - "handler" : "market", - "base" : { - "sounds" : { - "visit" : ["GAZEBO"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 2500, - "rmg" : { - "value" : 2500, - "rarity" : 20 - }, - "modes" : ["resource-skill"], - "title" : "core.genrltxt.602", - "speech" : "core.genrltxt.603", - "offer": - [ - { "noneOf" : ["necromancy"] }, - { "noneOf" : ["necromancy"] }, - { "noneOf" : ["necromancy"] }, - { "noneOf" : ["necromancy"] } - ] - } - } - }, "questGuard" : { "index" :215, "handler" : "questGuard", diff --git a/config/objects/markets.json b/config/objects/markets.json new file mode 100644 index 000000000..ea5c517b9 --- /dev/null +++ b/config/objects/markets.json @@ -0,0 +1,138 @@ +{ + "altarOfSacrifice" : { + "index" :2, + "handler" : "market", + "base" : { + "sounds" : { + "visit" : ["MYSTERY"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + "zoneLimit" : 1, + "value" : 100, + "rarity" : 20 + }, + "modes" : ["creature-experience", "artifact-experience"] + } + } + }, + + "tradingPost" : { + "index" :221, + "handler" : "market", + "base" : { + "sounds" : { + "ambient" : ["LOOPMARK"], + "visit" : ["STORE"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + "zoneLimit" : 1, + "value" : 100, + "rarity" : 100 + }, + "modes" : ["resource-resource", "resource-player"], + "efficiency" : 5 + } + } + }, + + "tradingPostDUPLICATE" : { + "index" :99, + "handler" : "market", + "base" : { + "sounds" : { + "ambient" : ["LOOPMARK"], + "visit" : ["STORE"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + "zoneLimit" : 1, + "value" : 100, + "rarity" : 100 + }, + "modes" : ["resource-resource", "resource-player"], + "efficiency" : 5 + } + } + }, + + "freelancersGuild" : { + "index" :213, + "handler" : "market", + "types" : { + "object" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + "zoneLimit" : 1, + "value" : 100, + "rarity" : 100 + }, + "modes" : ["creature-resource"] + } + } + }, + + "blackMarket" : { + "index" :7, + "handler" : "market", + "base" : { + "sounds" : { + "ambient" : ["LOOPMARK"], + "visit" : ["MYSTERY"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 8000, + "rmg" : { + "value" : 8000, + "rarity" : 20 + }, + "modes" : ["resource-artifact"] + } + } + }, + "university" : { + "index" :104, + "handler" : "market", + "base" : { + "sounds" : { + "visit" : ["GAZEBO"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 2500, + "rmg" : { + "value" : 2500, + "rarity" : 20 + }, + "modes" : ["resource-skill"], + "speech" : "@core.genrltxt.603", + "offer": + [ + { "noneOf" : ["necromancy"] }, + { "noneOf" : ["necromancy"] }, + { "noneOf" : ["necromancy"] }, + { "noneOf" : ["necromancy"] } + ] + } + } + } +} \ No newline at end of file diff --git a/config/schemas/gameSettings.json b/config/schemas/gameSettings.json index bf68e6087..9d1b55b1e 100644 --- a/config/schemas/gameSettings.json +++ b/config/schemas/gameSettings.json @@ -133,6 +133,14 @@ "originalFlyRules" : { "type" : "boolean" } } }, + "resources": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "weeklyBonusesAI" : { "type" : "object" } + } + }, + "spells": { "type" : "object", "additionalProperties" : false, diff --git a/config/schemas/market.json b/config/schemas/market.json new file mode 100644 index 000000000..7554b453a --- /dev/null +++ b/config/schemas/market.json @@ -0,0 +1,50 @@ +{ + "type" : "object", + "$schema" : "http://json-schema.org/draft-04/schema", + "title" : "VCMI map object format", + "description" : "Description of map object class", + "required" : [ "modes" ], + + "additionalProperties" : false, + + "properties" : { + "description" : { + "description" : "Message that will be shown on right-clicking this object", + "type" : "string" + }, + + "speech" : { + "description" : "Message that will be shown to player on visiting this object", + "type" : "string" + }, + + "modes" : { + "type" : "array", + "items" : { + "enum" : [ "resource-resource", "resource-player", "creature-resource", "resource-artifact", "artifact-resource", "artifact-experience", "creature-experience", "creature-undead", "resource-skill" ], + "type" : "string" + } + }, + "efficiency" : { + "type" : "number", + "minimum" : 1, + "maximum" : 9 + }, + "offer" : { + "type" : "array" + }, + + // Properties that might appear since this node is shared with object config + "compatibilityIdentifiers" : { }, + "blockedVisitable" : { }, + "removable" : { }, + "aiValue" : { }, + "index" : { }, + "base" : { }, + "name" : { }, + "rmg" : { }, + "templates" : { }, + "battleground" : { }, + "sounds" : { } + } +} diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 93ae2038f..94c95923f 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -620,7 +620,6 @@ "defaultRepositoryURL", "extraRepositoryURL", "extraRepositoryEnabled", - "enableInstalledMods", "autoCheckRepositories", "ignoreSslErrors", "updateOnStartup", @@ -647,10 +646,6 @@ "type" : "boolean", "default" : false }, - "enableInstalledMods" : { - "type" : "boolean", - "default" : true - }, "ignoreSslErrors" : { "type" : "boolean", "default" : false diff --git a/config/schemas/skill.json b/config/schemas/skill.json index 162f84de6..2332f0eaf 100644 --- a/config/schemas/skill.json +++ b/config/schemas/skill.json @@ -94,5 +94,9 @@ "onlyOnWaterMap" : { "type" : "boolean", "description" : "It true, skill won't be available on a map without water" + }, + "special" : { + "type" : "boolean", + "description" : "If true, skill is not available on maps at random" } } diff --git a/config/schemas/template.json b/config/schemas/template.json index 26ec3e3fe..4ed948449 100644 --- a/config/schemas/template.json +++ b/config/schemas/template.json @@ -12,7 +12,7 @@ "properties" : { "type" : { "type" : "string", - "enum" : ["playerStart", "cpuStart", "treasure", "junction"] + "enum" : ["playerStart", "cpuStart", "treasure", "junction", "sealed"] }, "size" : { "type" : "number", "minimum" : 1 }, "owner" : {}, diff --git a/docs/Readme.md b/docs/Readme.md index 284cf3c0b..68f3118cd 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,11 +1,11 @@ +# VCMI Project + [![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.0) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.6/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.6) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.7/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.7) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) -# VCMI Project - VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.

@@ -15,14 +15,13 @@ VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving New widget for Hero selection, featuring Pavillon Town

- ## Links - * Homepage: https://vcmi.eu/ - * Forums: https://forum.vcmi.eu/ - * Bugtracker: https://github.com/vcmi/vcmi/issues - * Discord: https://discord.gg/chBT42V - * GPT Store: https://chat.openai.com/g/g-1kNhX0mlO-vcmi-assistant +* Homepage: +* Forums: +* Bugtracker: +* Discord: +* GPT Store: ## Latest release @@ -31,6 +30,7 @@ Loading saves made with different major version of VCMI is usually **not** suppo Please see corresponding installation guide articles for details for your platform. ## Installation guides + - [Windows](players/Installation_Windows.md) - [macOS](players/Installation_macOS.md) - [Linux](players/Installation_Linux.md) @@ -70,6 +70,7 @@ See also installation guide for [Heroes Chronicles](players/Heroes_Chronicles.md ## Documentation and guidelines for developers Development environment setup instructions: + - [Building VCMI for Android](developers/Building_Android.md) - [Building VCMI for iOS](developers/Building_iOS.md) - [Building VCMI for Linux](developers/Building_Linux.md) @@ -78,6 +79,7 @@ Development environment setup instructions: - [Conan](developers/Conan.md) Engine documentation: (NOTE: may be outdated) + - [Development with Qt Creator](developers/Development_with_Qt_Creator.md) - [Coding Guidelines](developers/Coding_Guidelines.md) - [Bonus System](developers/Bonus_System.md) @@ -95,6 +97,6 @@ Engine documentation: (NOTE: may be outdated) ## Copyright and license VCMI Project source code is licensed under GPL version 2 or later. -VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: https://github.com/vcmi/vcmi-assets +VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: Copyright (C) 2007-2024 VCMI Team (check AUTHORS file for the contributors list) diff --git a/docs/developers/AI.md b/docs/developers/AI.md index b75ba539a..6fd7910ab 100644 --- a/docs/developers/AI.md +++ b/docs/developers/AI.md @@ -6,18 +6,19 @@ There are two types of AI: adventure and battle. **Battle AIs** are responsible for fighting, i.e. moving stacks on the battlefield We have 3 battle AIs so far: + * BattleAI - strongest * StupidAI - for neutrals, should be simple so that experienced players can abuse it * Empty AI - should do nothing at all. If needed another battle AI can be introduced. -Each battle AI consist of a few classes, but the main class, kind of entry point usually has the same name as the package itself. In BattleAI it is the BattleAI class. It implements some battle specific interface, do not remember. Main method there is activeStack(battle::Unit* stack). It is invoked by the system when it's time to move your stack. The thing you use to interact with the game and receive the gamestate is usually referenced in the code as cb. CPlayerSpecificCallback it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf(), which returns all units on the battlefield matching some lambda condition. -Each side in a battle is represented by an CArmedInstance object. CHeroInstance and CGDwelling, CGMonster and more are subclasses of CArmedInstance. CArmedInstance contains a set of stacks. When the battle starts, these stacks are converted to battle stacks. Usually Battle AIs reference them using the interface battle::Unit *. +Each battle AI consist of a few classes, but the main class, kind of entry point usually has the same name as the package itself. In BattleAI it is the BattleAI class. It implements some battle specific interface, do not remember. Main method there is `activeStack(battle::Unit * stack)`. It is invoked by the system when it's time to move your stack. The thing you use to interact with the game and receive the gamestate is usually referenced in the code as `cb`. `CPlayerSpecificCallback` it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf(), which returns all units on the battlefield matching some lambda condition. +Each side in a battle is represented by an `CArmedInstance` object. `CHeroInstance` and `CGDwelling`, `CGMonster` and more are subclasses of `CArmedInstance`. `CArmedInstance` contains a set of stacks. When the battle starts, these stacks are converted to battle stacks. Usually Battle AIs reference them using the interface `battle::Unit *`. Units have bonuses. Nearly everything aspect of a unit is configured in the form of bonuses. Attack, defense, health, retaliation, shooter or not, initial count of shots and so on. -When you call unit->getAttack() it summarizes all these bonuses and returns the resulting value. +When you call `unit->getAttack()` it summarizes all these bonuses and returns the resulting value. -One important class is HypotheticBattle. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around CPlayerSpecificCallback or another HypotheticBattle so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (CStackWithBonuses). So if you need to emulate an attack you can call hypotheticbattle.getforupdate() and it will return the CStackWithBonuses which you can safely change. +One important class is `HypotheticBattle`. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around `CPlayerSpecificCallback` or another `HypotheticBattle` so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (`CStackWithBonuses`). So if you need to emulate an attack you can call `hypotheticbattle.getforupdate()` and it will return the `CStackWithBonuses` which you can safely change. -## BattleAI +## BattleAI BattleAI's most important classes are the following: @@ -38,17 +39,20 @@ BattleAI itself handles all the rest and issues actual commands Adventure AI responsible for moving heroes on map, gathering things, developing town. Main idea is to gather all possible tasks on map, prioritize them and select the best one for each heroes. Initially was a fork of VCAI ### Parts + Gateway - a callback for server used to invoke AI actions when server thinks it is time to do something. Through this callback AI is informed about various events like hero level up, tile revialed, blocking dialogs and so on. In order to do this Gaateway implements specific interface. The interface is exactly the same for human and AI Another important actor for server interaction is CCallback * cb. This one is used to retrieve gamestate information and ask server to do things like hero moving, spell casting and so on. Each AI has own instance of Gateway and it is a root object which holds all AI state. Gateway has an event method yourTurn which invokes makeTurn in another thread. The last passes control to Nullkiller engine. Nullkiller engine - place where actual AI logic is organized. It contains a main loop for gathering and prioritizing things. Its algorithm: + * reset AI state, it avoids keeping some memory about the game in general to reduce amount of things serialized into savefile state. The only serialized things are in nullkiller->memory. This helps reducing save incompatibility. It should be mostly enough for AI to analyze data avaialble in CCallback * main loop, loop iteration is called a pass -** update AI state, some state is lazy and updates once per day to avoid performance hit, some state is recalculated each loop iteration. At this stage analysers and pathfidner work -** gathering goals, prioritizing and decomposing them -** execute selected best goals + * update AI state, some state is lazy and updates once per day to avoid performance hit, some state is recalculated each loop iteration. At this stage analysers and pathfidner work + * gathering goals, prioritizing and decomposing them + * execute selected best goals Analyzer - a module gathering data from CCallback *. Its goal to make some statistics and avoid making any significant decissions. + * HeroAnalyser - decides upong which hero suits better to be main (army carrier and fighter) and which is better to be a scout (gathering unguarded resources, exploring) * BuildAnalyzer - prepares information on what we can build in our towns, and what resources we need to do this * DangerHitMapAnalyser - checks if enemy hero can rich each tile, how fast and what is their army strangth @@ -61,9 +65,11 @@ Analyzer - a module gathering data from CCallback *. Its goal to make some stati * PriorityEvaluator - gathers information on task rewards, evaluates their priority using Fuzzy Light library (fuzzy logic) ### Goals + Units of activity in AI. Can be AbstractGoal, Task, Marker and Behavior Task - simple thing which can be done right away in order to gain some reward. Or a composition of simple things in case if more than one action is needed to gain the reward. + * AdventureSpellCast - town portal, water walk, air walk, summon boat * BuildBoat - builds a boat in a specific shipyard * BuildThis - builds a building in a specified town @@ -78,6 +84,7 @@ Task - simple thing which can be done right away in order to gain some reward. O * StayAtTown - stay at town for the rest of the day (to regain mana) Behavior - a core game activity + * CaptureObjectsBehavior - generally it is about visiting map objects which give reward. It can capture any object, even those which are behind monsters and so on. But due to performance considerations it is not allowed to handle monsters and quests now. * ClusterBehavior - uses information of ObjectClusterizer to unblock objects hidden behind various blockers. It kills guards, completes quests, captures garrisons. * BuildingBehavior - develops our towns @@ -89,6 +96,7 @@ Behavior - a core game activity * DefenceBehavior - defend towns by eliminating treatening heroes or hiding in town garrison AbstractGoal - some goals can not be completed because it is not clear how to do this. They express desire to do something, not exact plan. DeepDecomposer is used to refine such goals until they are turned into such plan or discarded. Some examples: + * CaptureObject - you need to visit some object (flag a shipyard for instance) but do not know how * CompleteQuest - you need to bypass bordergate or borderguard or questguard but do not know how AbstractGoal usually comes in form of composition with some elementar task blocked by abstract objective. For instance CaptureObject(Shipyard), ExecuteHeroChain(visit x, build boat, visit enemy town). When such composition is decomposed it can turn into either a pair of herochains or into another abstract composition if path to shipyard is also blocked with something. diff --git a/docs/developers/Bonus_System.md b/docs/developers/Bonus_System.md index e594fff8e..c3b86a646 100644 --- a/docs/developers/Bonus_System.md +++ b/docs/developers/Bonus_System.md @@ -6,8 +6,8 @@ The bonus system of VCMI is a set of mechanisms that make handling of different Each bonus originates from some node in the bonus system, and may have propagator and limiter objects attached to it. Bonuses are shared around as follows: -1. Bonuses with propagator are propagated to "matching" descendants in the red DAG - which descendants match is determined by the propagator. Bonuses without a propagator will not be propagated. -2. Bonuses without limiters are inherited by all descendants in the black DAG. If limiters are present, they can restrict inheritance to certain nodes. +1. Bonuses with propagator are propagated to "matching" descendants in the red DAG - which descendants match is determined by the propagator. Bonuses without a propagator will not be propagated. +2. Bonuses without limiters are inherited by all descendants in the black DAG. If limiters are present, they can restrict inheritance to certain nodes. Inheritance is the default means of sharing bonuses. A typical example is an artefact granting a bonus to attack/defense stat, which is inherited by the hero wearing it, and then by creatures in the hero's army. A common limiter is by creature - e.g. the hero Eric has a specialty that grants bonuses to attack, defense and speed, but only to griffins. @@ -15,9 +15,9 @@ Propagation is used when bonuses need to be shared in a different direction than ### Technical Details -- Propagation is done by copying bonuses to the target nodes. This happens when bonuses are added. -- Inheritance is done on-the-fly when needed, by traversing the black DAG. Results are cached to improve performance. -- Whenever a node changes (e.g. bonus added), a global counter gets increased which is used to check whether cached results are still current. +- Propagation is done by copying bonuses to the target nodes. This happens when bonuses are added. +- Inheritance is done on-the-fly when needed, by traversing the black DAG. Results are cached to improve performance. +- Whenever a node changes (e.g. bonus added), a global counter gets increased which is used to check whether cached results are still current. ## Operations on the graph @@ -26,6 +26,7 @@ There are two basic types of operations that can be performed on the graph: ### Adding a new node When node is attached to a new black parent (the only possibility - adding parent is the same as adding a child to it), the propagation system is triggered and works as follows: + - For the attached node and its all red ancestors - For every bonus - Call propagator giving the new descendant - then attach appropriately bonuses to the red descendant of attached node (or the node itself). @@ -54,7 +55,7 @@ Updaters are objects attached to bonuses. They can modify a bonus (typically by The following example shows an artifact providing a bonus based on the level of the hero that wears it: -```javascript +```json "core:greaterGnollsFlail": { "text" : { "description" : "This mighty flail increases the attack of all gnolls under the hero's command by twice the hero's level." }, diff --git a/docs/developers/Building_Android.md b/docs/developers/Building_Android.md index 5ce865f7b..c2517ba26 100644 --- a/docs/developers/Building_Android.md +++ b/docs/developers/Building_Android.md @@ -1,28 +1,28 @@ # Building Android -The following instructions apply to **v1.2 and later**. For earlier versions the best documentation is https://github.com/vcmi/vcmi-android/blob/master/building.txt (and reading scripts in that repo), however very limited to no support will be provided from our side if you wish to go down that rabbit hole. +The following instructions apply to **v1.2 and later**. For earlier versions the best documentation is (and reading scripts in that repo), however very limited to no support will be provided from our side if you wish to go down that rabbit hole. *Note*: building has been tested only on Linux and macOS. It may or may not work on Windows out of the box. ## Requirements -1. CMake 3.20+: download from your package manager or from https://cmake.org/download/ +1. CMake 3.20+: download from your package manager or from 2. JDK 11, not necessarily from Oracle -3. Android command line tools or Android Studio for your OS: https://developer.android.com/studio/ +3. Android command line tools or Android Studio for your OS: 4. Android NDK version **r25c (25.2.9519653)**, there're multiple ways to obtain it: - install with Android Studio - install with `sdkmanager` command line tool - - download from https://developer.android.com/ndk/downloads + - download from - download with Conan, see [#NDK and Conan](#ndk-and-conan) 5. Optional: - - Ninja: download from your package manager or from https://github.com/ninja-build/ninja/releases - - Ccache: download from your package manager or from https://github.com/ccache/ccache/releases + - Ninja: download from your package manager or from + - Ccache: download from your package manager or from ## Obtaining source code -Clone https://github.com/vcmi/vcmi with submodules. Example for command line: +Clone with submodules. Example for command line: -``` +```sh git clone --recurse-submodules https://github.com/vcmi/vcmi.git ``` @@ -31,6 +31,7 @@ git clone --recurse-submodules https://github.com/vcmi/vcmi.git We use Conan package manager to build/consume dependencies, find detailed usage instructions [here](./Conan.md). Note that the link points to the state of the current branch, for the latest release check the same document in the [master branch](https://github.com/vcmi/vcmi/blob/master/docs/developers/Сonan.md). On the step where you need to replace **PROFILE**, choose: + - `android-32` to build for 32-bit architecture (armeabi-v7a) - `android-64` to build for 64-bit architecture (aarch64-v8a) @@ -38,10 +39,10 @@ On the step where you need to replace **PROFILE**, choose: Conan must be aware of the NDK location when you execute `conan install`. There're multiple ways to achieve that as written in the [Conan docs](https://docs.conan.io/1/integrations/cross_platform/android.html): -- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. On the step where you need to replace **PROFILE**, choose _android-**X**-ndk_ where _**X**_ is either `32` or `64`. +- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. On the step where you need to replace **PROFILE**, choose *android-**X**-ndk* where ***X*** is either `32` or `64`. - to use an already installed NDK, you can simply pass it on the command line to `conan install`: (note that this will work only when consuming the pre-built binaries) -``` +```sh conan install -c tools.android:ndk_path=/path/to/ndk ... ``` diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index 4a9b42cf1..7fa79e487 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -11,15 +11,15 @@ Older distributions and compilers might work, but they aren't tested by Github C To compile, the following packages (and their development counterparts) are needed to build: -- CMake -- SDL2 with devel packages: mixer, image, ttf -- zlib and zlib-devel -- Boost C++ libraries v1.48+: program-options, filesystem, system, thread, locale -- Recommended, if you want to build launcher or map editor: Qt 5, widget and network modules -- Recommended, FFmpeg libraries, if you want to watch in-game videos: libavformat and libswscale. Their name could be libavformat-devel and libswscale-devel, or ffmpeg-libs-devel or similar names. -- Optional: - - if you want to build scripting modules: LuaJIT - - to speed up recompilation: Ccache +- CMake +- SDL2 with devel packages: mixer, image, ttf +- zlib and zlib-devel +- Boost C++ libraries v1.48+: program-options, filesystem, system, thread, locale +- Recommended, if you want to build launcher or map editor: Qt 5, widget and network modules +- Recommended, FFmpeg libraries, if you want to watch in-game videos: libavformat and libswscale. Their name could be libavformat-devel and libswscale-devel, or ffmpeg-libs-devel or similar names. +- Optional: + - if you want to build scripting modules: LuaJIT + - to speed up recompilation: Ccache ### On Debian-based systems (e.g. Ubuntu) @@ -41,7 +41,7 @@ NOTE: `fuzzylite-devel` package is no longer available in recent version of Fedo On Arch-based distributions, there is a development package available for VCMI on the AUR. -It can be found at https://aur.archlinux.org/packages/vcmi-git/ +It can be found at Information about building packages from the Arch User Repository (AUR) can be found at the Arch wiki. @@ -70,7 +70,7 @@ And put it into build directory. Then run `nix-shell` before running any build c We recommend the following directory structure: -``` +```text . ├── vcmi -> contains sources and is under git control └── build -> contains build output, makefiles, object files,... @@ -97,7 +97,7 @@ See [CMake](CMake.md) for a list of options ### Trigger build -``` +```sh cmake --build . -j8 ``` @@ -109,9 +109,9 @@ This will generate `vcmiclient`, `vcmiserver`, `vcmilauncher` as well as .so lib ### RPM package -The first step is to prepare a RPM build environment. On Fedora systems you can follow this guide: http://fedoraproject.org/wiki/How_to_create_an_RPM_package#SPEC_file_overview +The first step is to prepare a RPM build environment. On Fedora systems you can follow this guide: -0. Enable RPMFusion free repo to access to ffmpeg libs: +1. Enable RPMFusion free repo to access to ffmpeg libs: ```sh sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm @@ -120,33 +120,34 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele > [!NOTE] > The stock ffmpeg from Fedora repo is no good as it lacks a lots of codecs -1. Perform a git clone from a tagged branch for the right Fedora version from https://github.com/rpmfusion/vcmi; for example for Fedora 38:
git clone -b f38 --single-branch https://github.com/rpmfusion/vcmi.git
+2. Perform a git clone from a tagged branch for the right Fedora version from ; for example for Fedora 38:
git clone -b f38 --single-branch https://github.com/rpmfusion/vcmi.git
-2. Copy all files to ~/rpmbuild/SPECS with command:
cp vcmi/*  ~/rpmbuild/SPECS
+3. Copy all files to ~/rpmbuild/SPECS with command:
cp vcmi/*  ~/rpmbuild/SPECS
-3. Fetch all sources by using spectool: +4. Fetch all sources by using spectool: ```sh sudo dnf install rpmdevtools spectool -g -R ~/rpmbuild/SPECS/vcmi.spec ``` -4. Fetch all dependencies required to build the RPM: +5. Fetch all dependencies required to build the RPM: ```sh sudo dnf install dnf-plugins-core sudo dnf builddep ~/rpmbuild/SPECS/vcmi.spec ``` -4. Go to ~/rpmbuild/SPECS and open terminal in this folder and type: +6. Go to ~/rpmbuild/SPECS and open terminal in this folder and type: + ```sh rpmbuild -ba ~/rpmbuild/SPECS/vcmi.spec ``` -5. Generated RPM is in folder ~/rpmbuild/RPMS +7. Generated RPM is in folder ~/rpmbuild/RPMS If you want to package the generated RPM above for different processor architectures and operating systems you can use the tool mock. -Moreover, it is necessary to install mock-rpmfusion_free due to the packages ffmpeg-devel and ffmpeg-libs which aren't available in the standard RPM repositories(at least for Fedora). Go to ~/rpmbuild/SRPMS in terminal and type: +Moreover, it is necessary to install mock-rpmfusion_free due to the packages ffmpeg-devel and ffmpeg-libs which aren't available in the standard RPM repositories(at least for Fedora). Go to ~/rpmbuild/SRPMS in terminal and type: ```sh mock -r fedora-38-aarch64-rpmfusion_free path_to_source_RPM diff --git a/docs/developers/Building_Windows.md b/docs/developers/Building_Windows.md index e1f574125..8df20783d 100644 --- a/docs/developers/Building_Windows.md +++ b/docs/developers/Building_Windows.md @@ -12,8 +12,8 @@ Windows builds can be made in more than one way and with more than one tool. Thi - CMake [download link](https://cmake.org/download/). During install after accepting license agreement make sure to check "Add CMake to the system PATH for all users". - To unpack pre-build Vcpkg: [7-zip](http://www.7-zip.org/download.html) - Optional: - - To create installer: [NSIS](http://nsis.sourceforge.net/Main_Page) - - To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases) + - To create installer: [NSIS](http://nsis.sourceforge.net/Main_Page) + - To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases) ### Choose an installation directory @@ -21,12 +21,14 @@ Create a directory for VCMI development, eg. `C:\VCMI` We will call this directo Warning! Replace `%VCMI_DIR%` with path you've chosen for VCMI installation in the following commands. -It is recommended to avoid non-ascii characters in the path to your working folders. The folder should not be write-protected by system. +It is recommended to avoid non-ascii characters in the path to your working folders. The folder should not be write-protected by system. Good locations: + - `C:\VCMI` Bad locations: + - `C:\Users\Michał\VCMI (non-ascii character)` - `C:\Program Files (x86)\VCMI (write protection)` @@ -38,13 +40,14 @@ You have two options: to use pre-built libraries or build your own. We strongly #### Download and unpack archive -Vcpkg Archives are available at our GitHub: https://github.com/vcmi/vcmi-deps-windows/releases +Vcpkg Archives are available at our GitHub: - Download latest version available. EG: v1.6 assets - [vcpkg-export-x64-windows-v143.7z](https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-x64-windows-v143.7z) - Extract archive by right clicking on it and choosing "7-zip -> Extract Here". #### Move dependencies to target directory + Once extracted, a `vcpkg` directory will appear with `installed` and `scripts` subfolders inside. Move extracted `vcpkg` directory into your `%VCMI_DIR%` @@ -57,19 +60,21 @@ Be aware that building Vcpkg might take a lot of time depend on your CPU model a #### Clone vcpkg -1. open SourceTree -2. File -\> Clone -3. select **** as source -4. select **%VCMI_DIR%/vcpkg** as destination -5. click **Clone** +1. open SourceTree +2. File -\> Clone +3. select **** as source +4. select **%VCMI_DIR%/vcpkg** as destination +5. click **Clone** From command line use: - git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg +```sh +git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg +``` #### Build vcpkg and dependencies -- Run +- Run `%VCMI_DIR%/vcpkg/bootstrap-vcpkg.bat` - For 32-bit build run: `%VCMI_DIR%/vcpkg/vcpkg.exe install tbb:x64-windows fuzzylite:x64-windows sdl2:x64-windows sdl2-image:x64-windows sdl2-ttf:x64-windows sdl2-mixer[mpg123]:x64-windows boost:x64-windows qt5-base:x64-windows ffmpeg:x64-windows luajit:x64-windows` @@ -85,6 +90,7 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi ## Build VCMI #### From GIT GUI + - Open SourceTree - File -> Clone - select `https://github.com/vcmi/vcmi/` as source @@ -94,26 +100,30 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi - click Clone #### From command line + - `git clone --recursive https://github.com/vcmi/vcmi.git %VCMI_DIR%/source` ### Generate solution for VCMI + - Create `%VCMI_DIR%/build` folder - Open a command line prompt at `%VCMI_DIR%/build` -- Execute `cd %VCMI_DIR%/build` +- Execute `cd %VCMI_DIR%/build` - Create solution (Visual Studio 2022 64-bit) `cmake %VCMI_DIR%/source -DCMAKE_TOOLCHAIN_FILE=%VCMI_DIR%/vcpkg/scripts/buildsystems/vcpkg.cmake -G "Visual Studio 17 2022" -A x64` ### Compile VCMI with Visual Studio + - Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio - Select `Release` build type in the combobox - If you want to use ccache: - - Select `Manage Configurations...` in the combobox - - Specify the following CMake variable: `ENABLE_CCACHE=ON` - - See the [Visual Studio documentation](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170#cmake-variables-and-cache) for details + - Select `Manage Configurations...` in the combobox + - Specify the following CMake variable: `ENABLE_CCACHE=ON` + - See the [Visual Studio documentation](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170#cmake-variables-and-cache) for details - Right click on `BUILD_ALL` project. This `BUILD_ALL` project should be in `CMakePredefinedTargets` tree in Solution Explorer. - VCMI will be built in `%VCMI_DIR%/build/bin` folder! ### Compile VCMI with MinGW via MSYS2 -- Install MSYS2 from https://www.msys2.org/ + +- Install MSYS2 from - Start the `MSYS MinGW x64`-shell - Install dependencies: `pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-SDL2_ttf mingw-w64-x86_64-boost mingw-w64-x86_64-gcc mingw-w64-x86_64-ninja mingw-w64-x86_64-qt5-static mingw-w64-x86_64-qt5-tools mingw-w64-x86_64-tbb` - Generate and build solution from VCMI-root dir: `cmake --preset windows-mingw-release && cmake --build --preset windows-mingw-release` @@ -134,8 +144,10 @@ Vcpkg might be very unstable due to limited popularity and fact of using bleedin Pre-built version we provide is always manually tested with all supported versions of MSVC for both Release and Debug builds and all known quirks are listed below. -#$# Build is successful but can not start new game +### Build is successful but can not start new game + Make sure you have: + * Installed Heroes III from disk or using GOG installer * Copied `Data`, `Maps` and `Mp3` folders from Heroes III to: `%USERPROFILE%\Documents\My Games\vcmi\` diff --git a/docs/developers/Building_iOS.md b/docs/developers/Building_iOS.md index a922e44a1..f95f8f294 100644 --- a/docs/developers/Building_iOS.md +++ b/docs/developers/Building_iOS.md @@ -6,13 +6,13 @@ 2. Xcode: 3. CMake 3.21+: `brew install --cask cmake` or get from 4. Optional: - - CCache to speed up recompilation: `brew install ccache` + - CCache to speed up recompilation: `brew install ccache` ## Obtaining source code Clone with submodules. Example for command line: -``` +```sh git clone --recurse-submodules https://github.com/vcmi/vcmi.git ``` @@ -36,9 +36,8 @@ There're a few [CMake presets](https://cmake.org/cmake/help/latest/manual/cmake- Open terminal and `cd` to the directory with source code. Configuration example for device with Conan: -``` -cmake --preset ios-device-conan \ - -D BUNDLE_IDENTIFIER_PREFIX=com.MY-NAME +```sh +cmake --preset ios-device-conan -D BUNDLE_IDENTIFIER_PREFIX=com.MY-NAME ``` By default build directory containing Xcode project will appear at `../build-ios-device-conan`, but you can change it with `-B` option. @@ -61,7 +60,7 @@ You must also install game files, see [Installation on iOS](../players/Installat ### From command line -``` +```sh cmake --build --target vcmiclient -- -quiet ``` diff --git a/docs/developers/Building_macOS.md b/docs/developers/Building_macOS.md index 71a1ae644..b5a309581 100644 --- a/docs/developers/Building_macOS.md +++ b/docs/developers/Building_macOS.md @@ -15,7 +15,7 @@ Clone with submodules. Example for command line: -``` +```sh git clone --recurse-submodules https://github.com/vcmi/vcmi.git ``` @@ -91,7 +91,7 @@ Open `VCMI.xcodeproj` from the build directory, select `vcmiclient` scheme and h ## Packaging project into DMG file -After building, run `cpack` from the build directory. If using Xcode generator, also pass `-C ` with the same configuration that you used to build the project. +After building, run `cpack` from the build directory. If using Xcode generator, also pass `-C ` with the same configuration that you used to build the project. If you use Conan, it's expected that you use **conan-generated** directory at step 4 of [Conan package manager](Conan.md). diff --git a/docs/developers/CMake.md b/docs/developers/CMake.md index 140d7ef89..e4adadd48 100644 --- a/docs/developers/CMake.md +++ b/docs/developers/CMake.md @@ -1,23 +1,21 @@ # CMake options * `-D CMAKE_BUILD_TYPE=Debug` - * Enables debug info and disables optimizations + * Enables debug info and disables optimizations * `-D CMAKE_EXPORT_COMPILE_COMMANDS=ON` - * Creates `compile_commands.json` for [clangd](https://clangd.llvm.org/) language server. - - For clangd to find the JSON, create a file named `.clangd` with this content - ``` - CompileFlags: - CompilationDatabase: build - ``` - and place it here: - ``` - . - ├── vcmi -> contains sources and is under git control - ├── build -> contains build output, makefiles, object files,... - └── .clangd - ``` + * Creates `compile_commands.json` for [clangd](https://clangd.llvm.org/) language server. For clangd to find the JSON, create a file named `.clangd` with this content + ```text + CompileFlags: + CompilationDatabase: build + ``` + and place it here: + ```text + . + ├── vcmi -> contains sources and is under git control + ├── build -> contains build output, makefiles, object files,... + └── .clangd + ``` * `-D ENABLE_CCACHE:BOOL=ON` - * Speeds up recompilation + * Speeds up recompilation * `-G Ninja` - * Use Ninja build system instead of Make, which speeds up the build and doesn't require a `-j` flag \ No newline at end of file + * Use Ninja build system instead of Make, which speeds up the build and doesn't require a `-j` flag diff --git a/docs/developers/Code_Structure.md b/docs/developers/Code_Structure.md index 9f3e2c802..ee3384670 100644 --- a/docs/developers/Code_Structure.md +++ b/docs/developers/Code_Structure.md @@ -29,9 +29,10 @@ Most of VCMI configuration files uses Json format and located in "config" direct ### Main purposes of client Client is responsible for: -- displaying state of game to human player -- capturing player's actions and sending requests to server -- displaying changes in state of game indicated by server + +- displaying state of game to human player +- capturing player's actions and sending requests to server +- displaying changes in state of game indicated by server ### Rendering of graphics @@ -44,9 +45,9 @@ In rendering, Interface object system is quite helpful. Its base is CIntObject c Server is responsible for: -- maintaining state of the game -- handling requests from all clients participating in game -- informing all clients about changes in state of the game that are +- maintaining state of the game +- handling requests from all clients participating in game +- informing all clients about changes in state of the game that are visible to them ## Lib @@ -59,11 +60,11 @@ iOS platform pioneered single process build, where server is a static library an Lib contains code responsible for: -- handling most of Heroes III files (.lod, .txt setting files) -- storing information common to server and client like state of the game -- managing armies, buildings, artifacts, spells, bonuses and other game objects -- handling general game mechanics and related actions (only adventure map objects; it's an unwanted remnant of past development - all game mechanics should be handled by the server) -- networking and serialization +- handling most of Heroes III files (.lod, .txt setting files) +- storing information common to server and client like state of the game +- managing armies, buildings, artifacts, spells, bonuses and other game objects +- handling general game mechanics and related actions (only adventure map objects; it's an unwanted remnant of past development - all game mechanics should be handled by the server) +- networking and serialization #### Serialization @@ -94,7 +95,6 @@ Forward declarations of the lib in headers of other parts of the project need to `` `` - ##### New project part If you're creating new project part, place `VCMI_LIB_USING_NAMESPACE` in its `StdInc.h` to be able to use lib classes without explicit namespace in implementation files. Example: @@ -121,4 +121,4 @@ VCMI includes [FuzzyLite](http://code.google.com/p/fuzzy-lite/) library to make ### Duels -### ERM parser \ No newline at end of file +### ERM parser diff --git a/docs/developers/Coding_Guidelines.md b/docs/developers/Coding_Guidelines.md index 89bad05a6..5c1706cc3 100644 --- a/docs/developers/Coding_Guidelines.md +++ b/docs/developers/Coding_Guidelines.md @@ -4,7 +4,7 @@ VCMI implementation bases on C++17 standard. Any feature is acceptable as long as it's will pass build on our CI, but there is list below on what is already being used. -Any compiler supporting C++17 should work, but this has not been thoroughly tested. You can find information about extensions and compiler support at http://en.cppreference.com/w/cpp/compiler_support +Any compiler supporting C++17 should work, but this has not been thoroughly tested. You can find information about extensions and compiler support at ## Style Guidelines @@ -20,7 +20,7 @@ Inside a code block put the opening brace on the next line after the current sta Good: -``` cpp +```cpp if(a) { code(); @@ -30,7 +30,7 @@ if(a) Bad: -``` cpp +```cpp if(a) { code(); code(); @@ -41,14 +41,14 @@ Avoid using unnecessary open/close braces, vertical space is usually limited: Good: -``` cpp +```cpp if(a) code(); ``` Bad: -``` cpp +```cpp if(a) { code(); } @@ -58,7 +58,7 @@ Unless there are either multiple hierarchical conditions being used or that the Good: -``` cpp +```cpp if(a) { if(b) @@ -68,7 +68,7 @@ if(a) Bad: -``` cpp +```cpp if(a) if(b) code(); @@ -78,7 +78,7 @@ If there are brackets inside the body, outside brackets are required. Good: -``` cpp +```cpp if(a) { for(auto elem : list) @@ -90,7 +90,7 @@ if(a) Bad: -``` cpp +```cpp if(a) for(auto elem : list) { @@ -102,7 +102,7 @@ If "else" branch has brackets then "if" should also have brackets even if it is Good: -``` cpp +```cpp if(a) { code(); @@ -118,7 +118,7 @@ else Bad: -``` cpp +```cpp if(a) code(); else @@ -134,7 +134,7 @@ If you intentionally want to avoid usage of "else if" and keep if body indent ma Good: -``` cpp +```cpp if(a) { code(); @@ -148,7 +148,7 @@ else Bad: -``` cpp +```cpp if(a) code(); else @@ -160,7 +160,7 @@ When defining a method, use a new line for the brace, like this: Good: -``` cpp +```cpp void method() { } @@ -168,7 +168,7 @@ void method() Bad: -``` cpp +```cpp void Method() { } ``` @@ -179,14 +179,14 @@ Use white space in expressions liberally, except in the presence of parenthesis. **Good:** -``` cpp +```cpp if(a + 5 > method(blah('a') + 4)) foo += 24; ``` **Bad:** -``` cpp +```cpp if(a+5>method(blah('a')+4)) foo+=24; ``` @@ -199,13 +199,13 @@ Use a space before and after the address or pointer character in a pointer decla Good: -``` cpp +```cpp CIntObject * images[100]; ``` Bad: -``` cpp +```cpp CIntObject* images[100]; or CIntObject *images[100]; ``` @@ -214,14 +214,14 @@ Do not use spaces before parentheses. Good: -``` cpp +```cpp if(a) code(); ``` Bad: -``` cpp +```cpp if (a) code(); ``` @@ -230,7 +230,7 @@ Do not use extra spaces around conditions inside parentheses. Good: -``` cpp +```cpp if(a && b) code(); @@ -240,7 +240,7 @@ if(a && (b || c)) Bad: -``` cpp +```cpp if( a && b ) code(); @@ -252,14 +252,14 @@ Do not use more than one space between operators. Good: -``` cpp +```cpp if((a && b) || (c + 1 == d)) code(); ``` Bad: -``` cpp +```cpp if((a && b) || (c + 1 == d)) code(); @@ -273,14 +273,14 @@ When allocating objects, don't use parentheses for creating stack-based objects Good: -``` cpp +```cpp std::vector v; CGBoat btn = new CGBoat(); ``` Bad: -``` cpp +```cpp std::vector v(); // shouldn't compile anyway CGBoat btn = new CGBoat; ``` @@ -289,14 +289,14 @@ Avoid overuse of parentheses: Good: -``` cpp +```cpp if(a && (b + 1)) return c == d; ``` Bad: -``` cpp +```cpp if((a && (b + 1))) return (c == d); ``` @@ -305,7 +305,7 @@ if((a && (b + 1))) Base class list must be on same line with class name. -``` cpp +```cpp class CClass : public CClassBaseOne, public CClassBaseOne { int id; @@ -321,7 +321,7 @@ When 'private:', 'public:' and other labels are not on the line after opening br Good: -``` cpp +```cpp class CClass { int id; @@ -333,7 +333,7 @@ public: Bad: -``` cpp +```cpp class CClass { int id; @@ -344,7 +344,7 @@ public: Good: -``` cpp +```cpp class CClass { protected: @@ -357,7 +357,7 @@ public: Bad: -``` cpp +```cpp class CClass { @@ -373,7 +373,7 @@ public: Constructor member and base class initialization must be on new line, indented with tab with leading colon. -``` cpp +```cpp CClass::CClass() : CClassBaseOne(true, nullptr), id(0), bool parameters(false) { @@ -387,7 +387,7 @@ Switch statements have the case at the same indentation as the switch. Good: -``` cpp +```cpp switch(alignment) { case EAlignment::EVIL: @@ -407,7 +407,7 @@ default: Bad: -``` cpp +```cpp switch(alignment) { case EAlignment::EVIL: @@ -447,7 +447,7 @@ break; Good: -``` cpp +```cpp auto lambda = [this, a, &b](int3 & tile, int index) -> bool { do_that(); @@ -456,7 +456,7 @@ auto lambda = [this, a, &b](int3 & tile, int index) -> bool Bad: -``` cpp +```cpp auto lambda = [this,a,&b](int3 & tile, int index)->bool{do_that();}; ``` @@ -464,7 +464,7 @@ Empty parameter list is required even if function takes no arguments. Good: -``` cpp +```cpp auto lambda = []() { do_that(); @@ -473,7 +473,7 @@ auto lambda = []() Bad: -``` cpp +```cpp auto lambda = [] { do_that(); @@ -484,7 +484,7 @@ Do not use inline lambda expressions inside if-else, for and other conditions. Good: -``` cpp +```cpp auto lambda = []() { do_that(); @@ -497,7 +497,7 @@ if(lambda) Bad: -``` cpp +```cpp if([]() { do_that(); @@ -511,7 +511,7 @@ Do not pass inline lambda expressions as parameter unless it's the last paramete Good: -``` cpp +```cpp auto lambda = []() { do_that(); @@ -521,7 +521,7 @@ obj->someMethod(lambda, true); Bad: -``` cpp +```cpp obj->someMethod([]() { do_that(); @@ -530,7 +530,7 @@ obj->someMethod([]() Good: -``` cpp +```cpp obj->someMethod(true, []() { do_that(); @@ -543,7 +543,7 @@ Serialization of each element must be on it's own line since this make debugging Good: -``` cpp +```cpp template void serialize(Handler & h, const int version) { h & identifier; @@ -555,7 +555,7 @@ template void serialize(Handler & h, const int version) Bad: -``` cpp +```cpp template void serialize(Handler & h, const int version) { h & identifier & description & name & dependencies; @@ -566,7 +566,7 @@ Save backward compatibility code is exception when extra brackets are always use Good: -``` cpp +```cpp template void serialize(Handler & h, const int version) { h & identifier; @@ -586,7 +586,7 @@ template void serialize(Handler & h, const int version) Bad: -``` cpp +```cpp template void serialize(Handler & h, const int version) { h & identifier; @@ -604,7 +604,7 @@ template void serialize(Handler & h, const int version) For any new files, please paste the following info block at the very top of the source file: -``` cpp +```cpp /* * Name_of_File.h, part of VCMI engine * @@ -622,13 +622,13 @@ The above notice have to be included both in header and source files (.h/.cpp). For any header or source file code must be in following order: -1. Licensing information -2. pragma once preprocessor directive -3. include directives -4. Forward declarations -5. All other code +1. Licensing information +2. pragma once preprocessor directive +3. include directives +4. Forward declarations +5. All other code -``` cpp +```cpp /* * Name_of_File.h, part of VCMI engine * @@ -652,7 +652,7 @@ If you comment on the same line with code there must be one single space between Good: -``` cpp +```cpp if(a) { code(); //Do something @@ -665,7 +665,7 @@ else // Do something. Bad: -``` cpp +```cpp if(a) { code();//Do something @@ -680,7 +680,7 @@ If you add single-line comment on own line slashes must have same indent as code Good: -``` cpp +```cpp // Do something if(a) { @@ -692,7 +692,7 @@ if(a) Bad: -``` cpp +```cpp // Do something if(a) { @@ -706,7 +706,7 @@ Avoid comments inside multi-line if-else conditions. If your conditions are too Good: -``` cpp +```cpp bool isMyHeroAlive = a && b || (c + 1 > 15); bool canMyHeroMove = myTurn && hero.movePoints > 0; if(isMyHeroAlive && canMyHeroMove) @@ -717,7 +717,7 @@ if(isMyHeroAlive && canMyHeroMove) Bad: -``` cpp +```cpp if((a && b || (c + 1 > 15)) //Check if hero still alive && myTurn && hero.movePoints > 0) //Check if hero can move { @@ -727,7 +727,7 @@ if((a && b || (c + 1 > 15)) //Check if hero still alive You should write a comment before the class definition which describes shortly the class. 1-2 sentences are enough. Methods and class data members should be commented if they aren't self-describing only. Getters/Setters, simple methods where the purpose is clear or similar methods shouldn't be commented, because vertical space is usually limited. The style of documentation comments should be the three slashes-style: ///. -``` cpp +```cpp /// Returns true if a debug/trace log message will be logged, false if not. /// Useful if performance is important and concatenating the log message is a expensive task. bool isDebugEnabled() const; @@ -738,7 +738,7 @@ The above example doesn't follow a strict scheme on how to comment a method. It If you need a more detailed description for a method you can use such style: -``` cpp +```cpp /// /// /// @@ -749,7 +749,7 @@ If you need a more detailed description for a method you can use such style: /// @return Description of the return value ``` -A good essay about writing comments: http://ardalis.com/when-to-comment-your-code +A good essay about writing comments: ### Casing @@ -775,7 +775,7 @@ Outdated. There is separate entry for [Logging API](Logging_API.md) If you want to trace the control flow of VCMI, then you should use the macro LOG_TRACE or LOG_TRACE_PARAMS. The first one prints a message when the function is entered or leaved. The name of the function will also be logged. In addition to this the second macro, let's you specify parameters which you want to print. You should print traces with parameters like this: -``` cpp +```cpp LOG_TRACE_PARAMS(logGlobal, "hero '%s', spellId '%d', pos '%s'.", hero, spellId, pos); ``` @@ -797,14 +797,14 @@ Do not use uncommon abbreviations for class, method, parameter and global object Bad: -``` cpp +```cpp CArt * getRandomArt(...) class CIntObject ``` Good: -``` cpp +```cpp CArtifact * getRandomArtifact(...) class CInterfaceObject ``` @@ -827,7 +827,7 @@ The header StdInc.h should be included in every compilation unit. It has to be i Do not declare enumerations in global namespace. It is better to use strongly typed enum or to wrap them in class or namespace to avoid polluting global namespace: -``` cpp +```cpp enum class EAlignment { GOOD, @@ -848,7 +848,7 @@ namespace EAlignment If the comment duplicates the name of commented member, it's better if it wouldn't exist at all. It just increases maintenance cost. Bad: -``` cpp +```cpp size_t getHeroesCount(); //gets count of heroes (surprise?) ``` @@ -862,16 +862,16 @@ Don't return const objects or primitive types from functions -- it's pointless. Bad: -``` cpp +```cpp const std::vector guardingCreatures(int3 pos) const; ``` Good: -``` cpp +```cpp std::vector guardingCreatures(int3 pos) const; ``` ## Sources -[Mono project coding guidelines](http://www.mono-project.com/Coding_Guidelines) \ No newline at end of file +[Mono project coding guidelines](http://www.mono-project.com/Coding_Guidelines) diff --git a/docs/developers/Conan.md b/docs/developers/Conan.md index 8c754ce46..fb29795f8 100644 --- a/docs/developers/Conan.md +++ b/docs/developers/Conan.md @@ -27,7 +27,7 @@ The following platforms are supported and known to work, others might require ch - **Windows**: libraries are built with x86_64-mingw-w64-gcc version 10 (which is available in repositories of Ubuntu 22.04) - **Android**: libraries are built with NDK r25c (25.2.9519653) -2. Download the binaries archive and unpack it to `~/.conan` directory from https://github.com/vcmi/vcmi-dependencies/releases/latest +2. Download the binaries archive and unpack it to `~/.conan` directory from - macOS: pick **dependencies-mac-intel.txz** if you have Intel Mac, otherwise - **dependencies-mac-arm.txz** - iOS: pick ***dependencies-ios.txz*** @@ -65,7 +65,7 @@ If you use `--build=never` and this command fails, then it means that you can't VCMI "recipe" also has some options that you can specify. For example, if you don't care about game videos, you can disable FFmpeg dependency by passing `-o with_ffmpeg=False`. If you only want to make release build, you can use `GENERATE_ONLY_BUILT_CONFIG=1` environment variable to skip generating files for other configurations (our CI does this). -_Note_: you can find full reference of this command [in the official documentation](https://docs.conan.io/1/reference/commands/consumer/install.html) or by executing `conan help install`. +*Note*: you can find full reference of this command [in the official documentation](https://docs.conan.io/1/reference/commands/consumer/install.html) or by executing `conan help install`. ### Using our prebuilt binaries for macOS/iOS @@ -86,7 +86,7 @@ This subsection describes platform specifics to build libraries from source prop #### Building for macOS/iOS -- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your _host_ profile or pass `-s compiler.cppstd=11` on the command line. +- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your *host* profile or pass `-s compiler.cppstd=11` on the command line. - If you wish to build dependencies against system libraries (like our prebuilt ones do), follow [below instructions](#using-recipes-for-system-libraries) executing `conan create` for all directories. Don't forget to pass `-o with_apple_system_libs=True` to `conan install` afterwards. #### Building for Android @@ -105,11 +105,11 @@ After applying patch(es): 2. Run `make` 3. Copy file `qtbase/jar/QtAndroid.jar` from the build directory to the **package directory**, e.g. `~/.conan/data/qt/5.15.14/_/_/package/SOME_HASH/jar`. -_Note_: if you plan to build Qt from source again, then you don't need to perform the above _After applying patch(es)_ steps after building. +*Note*: if you plan to build Qt from source again, then you don't need to perform the above *After applying patch(es)* steps after building. ##### Using recipes for system libraries -1. Clone/download https://github.com/kambala-decapitator/conan-system-libs +1. Clone/download 2. Execute `conan create PACKAGE vcmi/CHANNEL`, where `PACKAGE` is a directory path in that repository and `CHANNEL` is **apple** for macOS/iOS and **android** for Android. Do it for each library you need. 3. Now you can execute `conan install` to build all dependencies. @@ -126,7 +126,7 @@ In these examples only the minimum required amount of options is passed to `cmak ### Use our prebuilt binaries to build for macOS x86_64 with Xcode -``` +```sh conan install . \ --install-folder=conan-generated \ --no-imports \ @@ -143,7 +143,7 @@ cmake -S . -B build -G Xcode \ If you also want to build the missing binaries from source, use `--build=missing` instead of `--build=never`. -``` +```sh conan install . \ --install-folder=~/my-dir \ --no-imports \ @@ -158,7 +158,7 @@ cmake -S . -B build \ ### Use our prebuilt binaries to build for iOS arm64 device with custom preset -``` +```sh conan install . \ --install-folder=~/my-dir \ --no-imports \ @@ -205,7 +205,7 @@ ubuntu Next steps are identical both in WSL and in real Ubuntu 22.04 -```bash +```sh sudo pip3 install conan sudo apt install cmake build-essential sed -i 's/x86_64-w64-mingw32/i686-w64-mingw32/g' CI/mingw-ubuntu/before-install.sh diff --git a/docs/developers/Development_with_Qt_Creator.md b/docs/developers/Development_with_Qt_Creator.md index 4e3c96f8e..66c621781 100644 --- a/docs/developers/Development_with_Qt_Creator.md +++ b/docs/developers/Development_with_Qt_Creator.md @@ -6,7 +6,7 @@ Qt Creator is the recommended IDE for VCMI development on Linux distributions, b - Almost no manual configuration when used with CMake. Project configuration is read from CMake text files, - Easy to setup and use with multiple different compiler toolchains: GCC, Visual Studio, Clang -You can install Qt Creator from repository, but better to stick to latest version from Qt website: https://www.qt.io/download-qt-installer-oss +You can install Qt Creator from repository, but better to stick to latest version from Qt website: ## Configuration @@ -21,4 +21,4 @@ The build dir should be set to something like /trunk/build for the debug build a There is a problem with QtCreator when debugging both vcmiclient and vcmiserver. If you debug the vcmiclient, start a game, attach the vcmiserver process to the gdb debugger(Debug \> Start Debugging \> Attach to Running External Application...) then breakpoints which are set for vcmiserver will be ignored. This looks like a bug, in any case it's not intuitively. Two workarounds are available luckily: 1. Run vcmiclient (no debug mode), then attach server process to the debugger -2. Open two instances of QtCreator and debug vcmiserver and vcmiclient separately(it works!) \ No newline at end of file +2. Open two instances of QtCreator and debug vcmiserver and vcmiclient separately (it works!) diff --git a/docs/developers/Logging_API.md b/docs/developers/Logging_API.md index a3ce23b76..5c43c91ed 100644 --- a/docs/developers/Logging_API.md +++ b/docs/developers/Logging_API.md @@ -2,14 +2,14 @@ ## Features -- A logger belongs to a "domain", this enables us to change log level settings more selectively -- The log format can be customized -- The color of a log entry can be customized based on logger domain and logger level -- Logger settings can be changed in the settings.json file -- No std::endl at the end of a log entry required -- Thread-safe -- Macros for tracing the application flow -- Provides stream-like and function-like logging +- A logger belongs to a "domain", this enables us to change log level settings more selectively +- The log format can be customized +- The color of a log entry can be customized based on logger domain and logger level +- Logger settings can be changed in the settings.json file +- No std::endl at the end of a log entry required +- Thread-safe +- Macros for tracing the application flow +- Provides stream-like and function-like logging ## Class diagram @@ -17,14 +17,14 @@ Some notes: -- There are two methods `configure` and `configureDefault` of the class `CBasicLogConfigurator` to initialize and setup the logging system. The latter one setups default logging and isn't dependent on VCMI's filesystem, whereas the first one setups logging based on the user's settings which can be configured in the settings.json. -- The methods `isDebugEnabled` and `isTraceEnabled` return true if a log record of level debug respectively trace will be logged. This can be useful if composing the log message is a expensive task and performance is important. +- There are two methods `configure` and `configureDefault` of the class `CBasicLogConfigurator` to initialize and setup the logging system. The latter one setups default logging and isn't dependent on VCMI's filesystem, whereas the first one setups logging based on the user's settings which can be configured in the settings.json. +- The methods `isDebugEnabled` and `isTraceEnabled` return true if a log record of level debug respectively trace will be logged. This can be useful if composing the log message is a expensive task and performance is important. ## Usage ### Setup settings.json -``` javascript +```json { "logging" : { "console" : { @@ -68,7 +68,7 @@ The following code shows how the logging system can be configured: If `configureDefault` or `configure` won't be called, then logs aren't written either to the console or to the file. The default logging setups a system like this: -**Console** +#### Console Format: %m Threshold: info @@ -76,17 +76,18 @@ coloredOutputEnabled: true colorMapping: trace -\> gray, debug -\> white, info -\> green, warn -\> yellow, error -\> red -**File** +#### File Format: %d %l %n \[%t\] - %m -**Loggers** +#### Loggers global -\> info ### How to get a logger There exist only one logger object per domain. A logger object cannot be copied. You can get access to a logger object by using the globally defined ones like `logGlobal` or `logAi`, etc... or by getting one manually: + ```cpp Logger * logger = CLogger::getLogger(CLoggerDomain("rmg")); ``` @@ -104,22 +105,22 @@ Don't include a '\n' or std::endl at the end of your log message, a new line wil The following list shows several log levels from the highest one to the lowest one: -- error -\> for errors, e.g. if resource is not available, if a initialization fault has occurred, if a exception has been thrown (can result in program termination) -- warn -\> for warnings, e.g. if sth. is wrong, but the program can continue execution "normally" -- info -\> informational messages, e.g. Filesystem initialized, Map loaded, Server started, etc... -- debug -\> for debugging, e.g. hero moved to (12,3,0), direction 3', 'following artifacts influence X: .. or pattern detected at pos (10,15,0), p-nr. 30, flip 1, repl. 'D' -- trace -\> for logging the control flow, the execution progress or fine-grained events, e.g. hero movement completed, entering CMapEditManager::updateTerrainViews: posx '10', posy '5', width '10', height '10', mapLevel '0',... +- error -\> for errors, e.g. if resource is not available, if a initialization fault has occurred, if a exception has been thrown (can result in program termination) +- warn -\> for warnings, e.g. if sth. is wrong, but the program can continue execution "normally" +- info -\> informational messages, e.g. Filesystem initialized, Map loaded, Server started, etc... +- debug -\> for debugging, e.g. hero moved to (12,3,0), direction 3', 'following artifacts influence X: .. or pattern detected at pos (10,15,0), p-nr. 30, flip 1, repl. 'D' +- trace -\> for logging the control flow, the execution progress or fine-grained events, e.g. hero movement completed, entering CMapEditManager::updateTerrainViews: posx '10', posy '5', width '10', height '10', mapLevel '0',... The following colors are available for console output: -- default -- green -- red -- magenta -- yellow -- white -- gray -- teal +- default +- green +- red +- magenta +- yellow +- white +- gray +- teal ### How to trace execution @@ -143,10 +144,10 @@ The program execution can be traced by using the macros TRACE_BEGIN, TRACE_END a A domain is a specific part of the software. In VCMI there exist several domains: -- network -- ai -- bonus -- network +- network +- ai +- bonus +- network In addition to these domains, there exist always a super domain called "global". Sub-domains can be created with "ai.battle" or "ai.adventure" for example. The dot between the "ai" and "battle" is important and notes the parent-child relationship of those two domains. A few examples how the log level will be inherited: diff --git a/docs/developers/Lua_Scripting_System.md b/docs/developers/Lua_Scripting_System.md index 16124c800..9968d7206 100644 --- a/docs/developers/Lua_Scripting_System.md +++ b/docs/developers/Lua_Scripting_System.md @@ -2,7 +2,7 @@ ## Configuration -``` javascript +```json { //general purpose script, Lua or ERM, runs on server "myScript": @@ -65,7 +65,7 @@ TODO **In near future Lua API may change drastically several times. Information #### Low level events API -``` Lua +```lua -- Each event type must be loaded first local PlayerGotTurn = require("events.PlayerGotTurn") @@ -87,75 +87,75 @@ VCMI uses LuaJIT, which is Lua 5.1 API, see [upstream documentation](https://www Following libraries are supported -- base -- table -- string -- math -- bit +- base +- table +- string +- math +- bit ## ERM ### Features -- no strict limit on function/variable numbers (technical limit 32 bit integer except 0)) -- TODO semi compare -- DONE macros +- no strict limit on function/variable numbers (technical limit 32 bit integer except 0)) +- TODO semi compare +- DONE macros ### Bugs -- TODO Broken XOR support (clashes with \`X\` option) +- TODO Broken XOR support (clashes with \`X\` option) ### Triggers -- TODO **!?AE** Equip/Unequip artifact -- WIP **!?BA** when any battle occurs -- WIP **!?BF** when a battlefield is prepared for a battle -- TODO **!?BG** at every action taken by any stack or by the hero -- TODO **!?BR** at every turn of a battle -- *!?CM (client only) click the mouse button.* -- TODO **!?CO** Commander triggers -- TODO **!?DL** Custom dialogs -- DONE **!?FU** function -- TODO **!?GE** "global" event -- TODO **!?GM** Saving/Loading -- TODO **!?HE** when the hero \# is attacked by an enemy hero or +- TODO **!?AE** Equip/Unequip artifact +- WIP **!?BA** when any battle occurs +- WIP **!?BF** when a battlefield is prepared for a battle +- TODO **!?BG** at every action taken by any stack or by the hero +- TODO **!?BR** at every turn of a battle +- *!?CM (client only) click the mouse button.* +- TODO **!?CO** Commander triggers +- TODO **!?DL** Custom dialogs +- DONE **!?FU** function +- TODO **!?GE** "global" event +- TODO **!?GM** Saving/Loading +- TODO **!?HE** when the hero \# is attacked by an enemy hero or visited by an allied hero -- TODO **!?HL** hero gains a level -- TODO **!?HM** every step a hero \# takes -- *!?IP Multiplayer support.* -- TODO **!?LE** (!$LE) An Event on the map -- WIP **!?MF** stack taking physical damage(before an action) -- TODO **!?MG** casting on the adventure map -- *!?MM scroll text during a battle* -- TODO **!?MR** Magic resistance -- TODO **!?MW** Wandering Monsters -- WIP **!?OB** (!$OB) visiting objects -- DONE **!?PI** Post Instruction. -- TODO **!?SN** Sound and ERA extensions -- *!?TH town hall* -- TODO **!?TL** Real-Time Timer -- TODO **!?TM** timed events +- TODO **!?HL** hero gains a level +- TODO **!?HM** every step a hero \# takes +- *!?IP Multiplayer support.* +- TODO **!?LE** (!$LE) An Event on the map +- WIP **!?MF** stack taking physical damage(before an action) +- TODO **!?MG** casting on the adventure map +- *!?MM scroll text during a battle* +- TODO **!?MR** Magic resistance +- TODO **!?MW** Wandering Monsters +- WIP **!?OB** (!$OB) visiting objects +- DONE **!?PI** Post Instruction. +- TODO **!?SN** Sound and ERA extensions +- *!?TH town hall* +- TODO **!?TL** Real-Time Timer +- TODO **!?TM** timed events ### Receivers #### VCMI -- **!!MC:S@varName@** - declare new "normal" variable (technically +- **!!MC:S@varName@** - declare new "normal" variable (technically v-var with string key) -- TODO Identifier resolver -- WIP Bonus system +- TODO Identifier resolver +- WIP Bonus system #### ERA -- DONE !!if !!el !!en -- TODO !!br !!co -- TODO !!SN:X +- DONE !!if !!el !!en +- TODO !!br !!co +- TODO !!SN:X #### WoG - TODO !!AR Артефакт (ресурс) в определенной позиции - TODO !!BA Битва - - !!BA:A$ return 1 for battle evaluation +- !!BA:A$ return 1 for battle evaluation - TODO !!BF Препятствия на поле боя - TODO !!BG Действий монстров в бою - TODO !!BH Действия героя в бою @@ -201,4 +201,4 @@ Following libraries are supported - *!#VC Контроль переменных* - WIP !!VR Установка переменных -### Persistence \ No newline at end of file +### Persistence diff --git a/docs/developers/Networking.md b/docs/developers/Networking.md index 5c3736abe..35407d496 100644 --- a/docs/developers/Networking.md +++ b/docs/developers/Networking.md @@ -5,12 +5,14 @@ For implementation details see files located at `lib/network` directory. VCMI uses connection using TCP to communicate with server, even in single-player games. However, even though TCP is stream-based protocol, VCMI uses atomic messages for communication. Each message is a serialized stream of bytes, preceded by 4-byte message size: -``` + +```cpp int32_t messageSize; byte messagePayload[messageSize]; ``` Networking can be used by: + - game client (vcmiclient / VCMI_Client.exe). Actual application that player interacts with directly using UI. - match server (vcmiserver / VCMI_Server.exe / part of game client). This app controls game logic and coordinates multiplayer games. - lobby server (vcmilobby). This app provides access to global lobby through which players can play game over Internet. @@ -28,12 +30,14 @@ For gameplay, VCMI serializes data into a binary stream. See [Serialization](Ser ## Global lobby communication For implementation details see: + - game client: `client/globalLobby/GlobalLobbyClient.h - match server: `server/GlobalLobbyProcessor.h - lobby server: `client/globalLobby/GlobalLobbyClient.h In case of global lobby, message payload uses plaintext json format - utf-8 encoded string: -``` + +```cpp int32_t messageSize; char jsonString[messageSize]; ``` @@ -43,6 +47,7 @@ Every message must be a struct (json object) that contains "type" field. Unlike ### Communication flow Notes: + - invalid message, such as corrupted json format or failure to validate message will result in no reply from server - in addition to specified messages, match server will send `operationFailed` message on failure to apply player request @@ -51,7 +56,8 @@ Notes: - client -> lobby: `clientRegister` - lobby -> client: `accountCreated` -#### Login +#### Login + - client -> lobby: `clientLogin` - lobby -> client: `loginSuccess` - lobby -> client: `chatHistory` @@ -59,10 +65,12 @@ Notes: - lobby -> client: `activeGameRooms` #### Chat Message + - client -> lobby: `sendChatMessage` - lobby -> every client: `chatMessage` #### New Game Room + - client starts match server instance - match -> lobby: `serverLogin` - lobby -> match: `loginSuccess` @@ -73,19 +81,23 @@ Notes: - lobby -> every client: `activeGameRooms` #### Joining a game room + See [#Proxy mode](proxy-mode) #### Leaving a game room + - client closes connection to match server - match -> lobby: `leaveGameRoom` -#### Sending an invite: +#### Sending an invite + - client -> lobby: `sendInvite` - lobby -> target client: `inviteReceived` Note: there is no dedicated procedure to accept an invite. Instead, invited player will use same flow as when joining public game room #### Logout + - client closes connection - lobby -> every client: `activeAccounts` @@ -94,6 +106,7 @@ Note: there is no dedicated procedure to accept an invite. Instead, invited play In order to connect players located behind NAT, VCMI lobby can operate in "proxy" mode. In this mode, connection will be act as proxy and will transmit gameplay data from client to a match server, without any data processing on lobby server. Currently, process to establish connection using proxy mode is: + - Player attempt to join open game room using `joinGameRoom` message - Lobby server validates requests and on success - notifies match server about new player in lobby using control connection - Match server receives request, establishes new connection to game lobby, sends `serverProxyLogin` message to lobby server and immediately transfers this connection to VCMIServer class to use as connection for gameplay communication @@ -101,4 +114,4 @@ Currently, process to establish connection using proxy mode is: - Game client receives message and establishes own side of proxy connection - connects to lobby, sends `clientProxyLogin` message and transfers to ServerHandler class to use as connection for gameplay communication - Lobby server accepts new connection and moves it into a proxy mode - all packages that will be received by one side of this connection will be re-sent to another side without any processing. -Once the game is over (or if one side disconnects) lobby server will close another side of the connection and erase proxy connection \ No newline at end of file +Once the game is over (or if one side disconnects) lobby server will close another side of the connection and erase proxy connection diff --git a/docs/developers/RMG_Description.md b/docs/developers/RMG_Description.md index 59a6b683a..33dca93d7 100644 --- a/docs/developers/RMG_Description.md +++ b/docs/developers/RMG_Description.md @@ -74,4 +74,4 @@ For every zone, a few random obstacle sets are selected. [Details](https://githu ### Filling space -Tiles which need to be `blocked` but are not `used` are filled with obstacles. Largest obstacles which cover the most tiles are picked first, other than that they are chosen randomly. \ No newline at end of file +Tiles which need to be `blocked` but are not `used` are filled with obstacles. Largest obstacles which cover the most tiles are picked first, other than that they are chosen randomly. diff --git a/docs/developers/Serialization.md b/docs/developers/Serialization.md index 143eaf0ad..56b428f1e 100644 --- a/docs/developers/Serialization.md +++ b/docs/developers/Serialization.md @@ -140,7 +140,7 @@ CLoadFile/CSaveFile classes allow to read data to file and store data to file. T #### Networking -See [Networking](Networking.md) +See [Networking](Networking.md) ### Additional features @@ -259,4 +259,4 @@ Foo *loadedA, *loadedB; The feature recognizes pointers by addresses. Therefore it allows mixing pointers to base and derived classes. However, it does not allow serializing classes with multiple inheritance using a "non-first" base (other bases have a certain address offset from the actual object). -Pointer cycles are properly handled. This feature makes sense for savegames and is turned on for them. \ No newline at end of file +Pointer cycles are properly handled. This feature makes sense for savegames and is turned on for them. diff --git a/docs/maintainers/Project_Infrastructure.md b/docs/maintainers/Project_Infrastructure.md index a55e6ad41..41032fc54 100644 --- a/docs/maintainers/Project_Infrastructure.md +++ b/docs/maintainers/Project_Infrastructure.md @@ -9,30 +9,30 @@ So far we using following services: ### Most important - VCMI.eu domain paid until July of 2019. - - Owner: Tow - - Our main domain used by services. + - Owner: Tow + - Our main domain used by services. - VCMI.download paid until November of 2026. - - Owner: SXX - - Intended to be used for all assets downloads. - - Domain registered on GANDI and **can be renewed by anyone without access to account**. + - Owner: SXX + - Intended to be used for all assets downloads. + - Domain registered on GANDI and **can be renewed by anyone without access to account**. - [DigitalOcean](https://cloud.digitalocean.com/) team. - - Our hosting sponsor. - - Administrator access: SXX, Warmonger. - - User access: AVS, Tow. + - Our hosting sponsor. + - Administrator access: SXX, Warmonger. + - User access: AVS, Tow. - [CloudFlare](https://www.cloudflare.com/a/overview) account. - - Access through shared login / password. - - All of our infrastructure is behind CloudFlare and all our web. We manage our DNS there. + - Access through shared login / password. + - All of our infrastructure is behind CloudFlare and all our web. We manage our DNS there. - [Google Apps (G Suite)](https://admin.google.com/) account. - - It's only for vcmi.eu domain and limited to 5 users. Each account has limit of 500 emails / day. - - One administrative email used for other services registration. - - "noreply" email used for outgoing mail on Wiki and Bug Tracker. - - "forum" email used for outgoing mail on Forums. Since we authenticate everyone through forum it's should be separate email. - - Administrator access: Tow, SXX. + - It's only for vcmi.eu domain and limited to 5 users. Each account has limit of 500 emails / day. + - One administrative email used for other services registration. + - "noreply" email used for outgoing mail on Wiki and Bug Tracker. + - "forum" email used for outgoing mail on Forums. Since we authenticate everyone through forum it's should be separate email. + - Administrator access: Tow, SXX. - [Google Play Console](https://play.google.com/apps/publish/) account. - - Hold ownership over VCMI Android App. - - Owner: SXX - - Administrator access: Warmonger, AVS, Ivan. - - Release manager access: Fay. + - Hold ownership over VCMI Android App. + - Owner: SXX + - Administrator access: Warmonger, AVS, Ivan. + - Release manager access: Fay. Not all services let us safely share login credentials, but at least when possible at least two of core developers must have access to them in case of emergency. @@ -41,20 +41,20 @@ Not all services let us safely share login credentials, but at least when possib We want to notify players about updates on as many social services as possible. - Facebook page: - - Administrator access: SXX, Warmonger + - Administrator access: SXX, Warmonger - Twitter account: - - Administrator access: SXX. - - User access via TweetDeck: -- VK / VKontakte page: - - Owner: SXX - - Administrator access: AVS + - Administrator access: SXX. +- User access via TweetDeck: + - VK / VKontakte page: +- Owner: SXX + - Administrator access: AVS - Steam group: - - Administrator access: SXX - - Moderator access: Dydzio -- Reddit: - - Administrator access: SXX -- ModDB entry: - - Administrator access: SXX + - Administrator access: SXX +- Moderator access: Dydzio + - Reddit: +- Administrator access: SXX + - ModDB entry: +- Administrator access: SXX ### Communication channels @@ -70,48 +70,46 @@ We want to notify players about updates on as many social services as possible. ### Other services - Launchpad PPA: - - Member access: AVS - - Administrator access: Ivan, SXX + - Member access: AVS + - Administrator access: Ivan, SXX - Snapcraft Dashboard: - - Administrator access: SXX + - Administrator access: SXX - Coverity Scan page: - - Administrator access: SXX, Warmonger, AVS + - Administrator access: SXX, Warmonger, AVS - OpenHub page: - - Administrator access: Tow + - Administrator access: Tow - Docker Hub organization: - - Administrator access: SXX + - Administrator access: SXX Reserve accounts for other code hosting services: - GitLab organization: - - Administrator access: SXX + - Administrator access: SXX - BitBucket organization: - - Administrator access: SXX + - Administrator access: SXX ## What's to improve -1. Encourage Tow to transfer VCMI.eu to GANDI so it's can be also renewed without access. -2. Use 2FA on CloudFlare and just ask everyone to get FreeOTP and then use shared secret. -3. Centralized way to post news about game updates to all social media. +1. Encourage Tow to transfer VCMI.eu to GANDI so it's can be also renewed without access. +2. Use 2FA on CloudFlare and just ask everyone to get FreeOTP and then use shared secret. +3. Centralized way to post news about game updates to all social media. -# Project Servers Configuration +## Project Servers Configuration This section dedicated to explain specific configurations of our servers for anyone who might need to improve it in future. -## Droplet configuration - -### Droplet and hosted services +### Droplet configuration Currently we using two droplets: - First one serve all of our web services: - - [Forum](https://forum.vcmi.eu/) - - [Bug tracker](https://bugs.vcmi.eu/) - - [Wiki](https://wiki.vcmi.eu/) - - [Slack invite page](https://slack.vcmi.eu/) + - [Forum](https://forum.vcmi.eu/) + - [Bug tracker](https://bugs.vcmi.eu/) + - [Wiki](https://wiki.vcmi.eu/) + - [Slack invite page](https://slack.vcmi.eu/) - Second serve downloads: - - [Legacy download page](http://download.vcmi.eu/) - - [Build download page](https://builds.vcmi.download/) + - [Legacy download page](http://download.vcmi.eu/) + - [Build download page](https://builds.vcmi.download/) To keep everything secure we should always keep binary downloads separate from any web services. @@ -131,4 +129,4 @@ We only expose floating IP that can be detached from droplet in case of emergenc - Address: beholder.vcmi.eu (67.207.75.182) - Port 22 serve SFTP for file uploads as well as CI artifacts uploads. -If new services added firewall rules can be adjusted in [DO control panel](https://cloud.digitalocean.com/networking/firewalls). \ No newline at end of file +If new services added firewall rules can be adjusted in [DO control panel](https://cloud.digitalocean.com/networking/firewalls). diff --git a/docs/maintainers/Release_Process.md b/docs/maintainers/Release_Process.md index 51d7a8e1a..32456c1a4 100644 --- a/docs/maintainers/Release_Process.md +++ b/docs/maintainers/Release_Process.md @@ -1,12 +1,16 @@ # Release Process ## Versioning + For releases VCMI uses version numbering in form "1.X.Y", where: + - 'X' indicates major release. Different major versions are generally not compatible with each other. Save format is different, network protocol is different, mod format likely different. - 'Y' indicates hotfix release. Despite its name this is usually not urgent, but planned release. Different hotfixes for same major version are fully compatible with each other. ## Branches + Our branching strategy is very similar to GitFlow: + - `master` branch has release commits. One commit - one release. Each release commit should be tagged with version `1.X.Y` when corresponding version is released. State of master branch represents state of latest public release. - `beta` branch is for stabilization of ongoing release. Beta branch is created when new major release enters stabilization stage and is used for both major release itself as well as for subsequent hotfixes. Only changes that are safe, have minimal chance of regressions and improve player experience should be targeted into this branch. Breaking changes (e.g. save format changes) are forbidden in beta. - `develop` branch is a main branch for ongoing development. Pull requests with new features should be targeted to this branch, `develop` version is one major release ahead of `beta`. @@ -14,12 +18,14 @@ Our branching strategy is very similar to GitFlow: ## Release process step-by-step ### Initial release setup (major releases only) + Should be done immediately after start of stabilization stage for previous release - Create project named `Release 1.X` - Add all features and bugs that should be fixed as part of this release into this project ### Start of stabilization stage (major releases only) + Should be done 2 weeks before planned release date. All major features should be finished at this point. - Create `beta` branch from `develop` @@ -34,6 +40,7 @@ Should be done 2 weeks before planned release date. All major features should be - Bump version and build ID for Android on `beta` branch ### Release preparation stage + Should be done 1 week before release. Release date should be decided at this point. - Make sure to announce codebase freeze deadline (1 day before release) to all developers @@ -45,21 +52,23 @@ Should be done 1 week before release. Release date should be decided at this poi - - Update downloads counter in `docs/readme.md` ### Release preparation stage + Should be done 1 day before release. At this point beta branch is in full freeze. - Merge release preparation PR into `beta` - Merge `beta` into `master`. This will trigger CI pipeline that will generate release packages - Create draft release page, specify `1.x.y` as tag for `master` after publishing - Check that artifacts for all platforms have been built by CI on `master` branch -- Download and rename all build artifacts to use form "VCMI-1.X.Y-Platform.xxx" +- Download and rename all build artifacts to use form `VCMI-1.X.Y-Platform.xxx` - Attach build artifacts for all platforms to release page - Manually extract Windows installer, remove `$PLUGINSDIR` directory which contains installer files and repackage data as .zip archive - Attach produced zip archive to release page as an alternative Windows installer - Upload built AAB to Google Play and send created release draft for review (usually takes several hours) - Prepare pull request for [vcmi-updates](https://github.com/vcmi/vcmi-updates) -- (major releases only) Prepare pull request with release update for web site https://github.com/vcmi/VCMI.eu +- (major releases only) Prepare pull request with release update for web site ### Release publishing phase + Should be done on release date - Trigger builds for new release on Ubuntu PPA diff --git a/docs/maintainers/Ubuntu_PPA.md b/docs/maintainers/Ubuntu_PPA.md index 8ada53d86..1dbd6a7d0 100644 --- a/docs/maintainers/Ubuntu_PPA.md +++ b/docs/maintainers/Ubuntu_PPA.md @@ -1,6 +1,7 @@ # Ubuntu PPA ## Main links + - [Team](https://launchpad.net/~vcmi) - [Project](https://launchpad.net/vcmi) - [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) @@ -14,31 +15,42 @@ ## Automatic daily builds process ### Code import + - Launchpad performs regular (once per few hours) clone of our git repository. - This process can be observed on [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) page. - If necessary, it is possible to trigger fresh clone immediately (Import Now button) + ### Build dependencies + - All packages required for building of vcmi are defined in [debian/control](https://github.com/vcmi/vcmi/blob/develop/debian/control) file - Launchpad will automatically install build dependencies during build - Dependencies of output .deb package are defined implicitly as dependencies of packages required for build + ### Recipe building + - Every 24 hours Launchpad triggers daily builds on all recipes that have build schedule enable. For vcmi this is [Daily recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-daily) - Alternatively, builds can be triggered manually using "request build(s) link on recipe page. VCMI uses this for [Stable recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-stable) + ### Recipe content (build settings) + - Version of resulting .deb package is set in recipe content, e.g `{debupstream}+git{revtime}` for daily builds - Base version (referred as `debupstream` on Launchpad is taken from source code, [debian/changelog](https://github.com/vcmi/vcmi/blob/develop/debian/changelog) file - CMake configuration settings are taken from source code, [debian/rules](https://github.com/vcmi/vcmi/blob/develop/debian/rules) file - Branch which is used for build is specified in recipe content, e.g. `lp:vcmi master` + ## Workflow for creating a release build + - if necessary, push all required changes including `debian/changelog` update to `vcmi/master` branch - Go to [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) and run repository import. - Wait for import to finish, which usually happens within a minute. Press F5 to actually see changes. - Go to [Stable recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-stable) and request new builds - Wait for builds to finish. This takes quite a while, usually - over a hour, even more for arm builds - Once built, all successfully built packages are automatically copied to PPA linked to the recipe -- If any of builds have failed, open page with build info and check logs. +- If any of builds have failed, open page with build info and check logs. + ## People with access -- [alexvins](https://github.com/alexvins) (https://launchpad.net/~alexvins) -- [ArseniyShestakov](https://github.com/ArseniyShestakov) (https://launchpad.net/~sxx) -- [IvanSavenko](https://github.com/IvanSavenko) (https://launchpad.net/~saven-ivan) -- (Not member of VCMI, creator of PPA) (https://launchpad.net/~mantas) \ No newline at end of file + +- [alexvins](https://github.com/alexvins) () +- [ArseniyShestakov](https://github.com/ArseniyShestakov) () +- [IvanSavenko](https://github.com/IvanSavenko) () +- (Not member of VCMI, creator of PPA) () diff --git a/docs/modders/Animation_Format.md b/docs/modders/Animation_Format.md index 2aece5165..8b3c44082 100644 --- a/docs/modders/Animation_Format.md +++ b/docs/modders/Animation_Format.md @@ -2,13 +2,13 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def this format allows: -- Overriding individual frames from json file (e.g. icons) -- Modern graphics formats (targa, png - all formats supported by VCMI image loader) -- Does not requires any special tools - all you need is text editor and images. +- Overriding individual frames from json file (e.g. icons) +- Modern graphics formats (targa, png - all formats supported by VCMI image loader) +- Does not requires any special tools - all you need is text editor and images. ## Format description -``` javascript +```json { // Base path of all images in animation. Optional. // Can be used to avoid using long path to images @@ -58,12 +58,14 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def ### Replacing a button This json file will allow replacing .def file for a button with png images. Buttons require following images: + 1. Active state. Button is active and can be pressed by player 2. Pressed state. Player pressed button but have not released it yet 3. Blocked state. Button is blocked and can not be interacted with. Note that some buttons are never blocked and can be used without this image 4. Highlighted state. This state is used by only some buttons and only in some cases. For example, in main menu buttons will appear highlighted when mouse cursor is on top of the image. Another example is buttons that can be selected, such as settings that can be toggled on or off -```javascript +```json +{ "basepath" : "interface/MyButton", // all images are located in this directory "images" : @@ -80,7 +82,8 @@ This json file will allow replacing .def file for a button with png images. Butt This json file allows defining one animation sequence, for example for adventure map objects or for town buildings. -```javascript +```json +{ "basepath" : "myTown/myBuilding", // all images are located in this directory "sequences" : diff --git a/docs/modders/Bonus/Bonus_Duration_Types.md b/docs/modders/Bonus/Bonus_Duration_Types.md index 3c20d0e7c..c82448116 100644 --- a/docs/modders/Bonus/Bonus_Duration_Types.md +++ b/docs/modders/Bonus/Bonus_Duration_Types.md @@ -4,14 +4,14 @@ Bonus may have any of these durations. They acts in disjunction. ## List of all bonus duration types -- PERMANENT -- ONE_BATTLE: at the end of battle -- ONE_DAY: at the end of day -- ONE_WEEK: at the end of week (bonus lasts till the end of week, NOT 7 days) -- N_TURNS: used during battles, after battle bonus is always removed -- N_DAYS -- UNTIL_BEING_ATTACKED: removed after any damage-inflicting attack -- UNTIL_ATTACK: removed after attack and counterattacks are performed -- STACK_GETS_TURN: removed when stack gets its turn - used for defensive stance -- COMMANDER_KILLED -- UNTIL_OWN_ATTACK: removed after attack (not counterattack) is performed \ No newline at end of file +- PERMANENT +- ONE_BATTLE: at the end of battle +- ONE_DAY: at the end of day +- ONE_WEEK: at the end of week (bonus lasts till the end of week, NOT 7 days) +- N_TURNS: used during battles, after battle bonus is always removed +- N_DAYS +- UNTIL_BEING_ATTACKED: removed after any damage-inflicting attack +- UNTIL_ATTACK: removed after attack and counterattacks are performed +- STACK_GETS_TURN: removed when stack gets its turn - used for defensive stance +- COMMANDER_KILLED +- UNTIL_OWN_ATTACK: removed after attack (not counterattack) is performed diff --git a/docs/modders/Bonus/Bonus_Limiters.md b/docs/modders/Bonus/Bonus_Limiters.md index 5dc9ee46a..304f6c043 100644 --- a/docs/modders/Bonus/Bonus_Limiters.md +++ b/docs/modders/Bonus/Bonus_Limiters.md @@ -15,7 +15,7 @@ The limiters take no parameters: Example: -``` javascript +```json "limiters" : [ "SHOOTER_ONLY" ] ``` @@ -25,12 +25,12 @@ Example: Parameters: -- Bonus type -- (optional) bonus subtype -- (optional) bonus sourceType and sourceId in struct -- example: (from Adele's bless): +- Bonus type +- (optional) bonus subtype +- (optional) bonus sourceType and sourceId in struct +- example: (from Adele's bless): -``` javascript +```json "limiters" : [ { "type" : "HAS_ANOTHER_BONUS_LIMITER", @@ -50,20 +50,21 @@ Parameters: Parameters: -- Creature id (string) -- (optional) include upgrades - default is false +- Creature id (string) +- (optional) include upgrades - default is false ### CREATURE_ALIGNMENT_LIMITER Parameters: -- Alignment identifier +- Alignment identifier ### CREATURE_LEVEL_LIMITER If parameters is empty, level limiter works as CREATURES_ONLY limiter Parameters: + - Minimal level - Maximal level @@ -71,24 +72,24 @@ Parameters: Parameters: -- Faction identifier +- Faction identifier ### CREATURE_TERRAIN_LIMITER Parameters: -- Terrain identifier +- Terrain identifier Example: -``` javascript +```json "limiters": [ { "type":"CREATURE_TYPE_LIMITER", "parameters": [ "angel", true ] } ], ``` -``` javascript +```json "limiters" : [ { "type" : "CREATURE_TERRAIN_LIMITER", "parameters" : ["sand"] @@ -106,13 +107,13 @@ Parameters: The following limiters must be specified as the first element of a list, and operate on the remaining limiters in that list: -- allOf (default when no aggregate limiter is specified) -- anyOf -- noneOf +- allOf (default when no aggregate limiter is specified) +- anyOf +- noneOf Example: -``` javascript +```json "limiters" : [ "noneOf", "IS_UNDEAD", @@ -121,4 +122,4 @@ Example: "parameters" : [ "SIEGE_WEAPON" ] } ] -``` \ No newline at end of file +``` diff --git a/docs/modders/Bonus/Bonus_Propagators.md b/docs/modders/Bonus/Bonus_Propagators.md index 90f09cf84..2a3e93e20 100644 --- a/docs/modders/Bonus/Bonus_Propagators.md +++ b/docs/modders/Bonus/Bonus_Propagators.md @@ -2,9 +2,9 @@ ## Available propagators -- BATTLE_WIDE: Affects both sides during battle -- VISITED_TOWN_AND_VISITOR: Used with Legion artifacts (town visited by hero) -- PLAYER_PROPAGATOR: Bonus will affect all objects owned by player. Used by Statue of Legion. -- HERO: Bonus will be transferred to hero (for example from stacks in his army). -- TEAM_PROPAGATOR: Bonus will affect all objects owned by player and his allies. -- GLOBAL_EFFECT: This effect will influence all creatures, heroes and towns on the map. \ No newline at end of file +- BATTLE_WIDE: Affects both sides during battle +- VISITED_TOWN_AND_VISITOR: Used with Legion artifacts (town visited by hero) +- PLAYER_PROPAGATOR: Bonus will affect all objects owned by player. Used by Statue of Legion. +- HERO: Bonus will be transferred to hero (for example from stacks in his army). +- TEAM_PROPAGATOR: Bonus will affect all objects owned by player and his allies. +- GLOBAL_EFFECT: This effect will influence all creatures, heroes and towns on the map. diff --git a/docs/modders/Bonus/Bonus_Range_Types.md b/docs/modders/Bonus/Bonus_Range_Types.md index 5be67c077..c3b5567ef 100644 --- a/docs/modders/Bonus/Bonus_Range_Types.md +++ b/docs/modders/Bonus/Bonus_Range_Types.md @@ -10,10 +10,10 @@ TODO: ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT range types work ONLY with creature For replacing ONLY_ENEMY_ARMY alias, you should use the following parameters of bonus: -``` +```text "propagator": "BATTLE_WIDE", "propagationUpdater" : "BONUS_OWNER_UPDATER", "limiters" : [ "OPPOSITE_SIDE" ] ``` -If some propagators was set before, it was actually ignored and should be replaced to code above. And OPPOSITE_SIDE limiter should be first, if any other limiters exists. \ No newline at end of file +If some propagators was set before, it was actually ignored and should be replaced to code above. And OPPOSITE_SIDE limiter should be first, if any other limiters exists. diff --git a/docs/modders/Bonus/Bonus_Sources.md b/docs/modders/Bonus/Bonus_Sources.md index 86754e407..c23140132 100644 --- a/docs/modders/Bonus/Bonus_Sources.md +++ b/docs/modders/Bonus/Bonus_Sources.md @@ -19,4 +19,4 @@ - STACK_EXPERIENCE - COMMANDER - GLOBAL -- OTHER \ No newline at end of file +- OTHER diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 5d36ec149..640c05e54 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -57,9 +57,9 @@ Bonus that does not account for propagation and gives extra resources per day wi Increases amount of movement points available to affected hero on new turn -- subtype: - - heroMovementLand: only land movement will be affected - - heroMovementSea: only sea movement will be affected +- subtype: + - heroMovementLand: only land movement will be affected + - heroMovementSea: only sea movement will be affected - val: number of movement points (100 points for a tile) ### WATER_WALKING @@ -128,7 +128,7 @@ Allows to raise different creatures than Skeletons after battle. - addInfo: Level of Necromancy secondary skill (1 - Basic, 3 - Expert) - Example (from Cloak Of The Undead King): -```jsonc +```json { "type" : "IMPROVED_NECROMANCY", "subtype" : "creature.walkingDead", @@ -158,7 +158,7 @@ Allows affected heroes to learn spells from each other during hero exchange Reduces movement points penalty when moving on terrains with movement cost over 100 points. Can not reduce movement cost below 100 points -- val: penalty reduction, in movement points per tile. +- val: penalty reduction, in movement points per tile. ### WANDERING_CREATURES_JOIN_BONUS @@ -256,7 +256,7 @@ Gives creature under effect of this spell additional bonus, which is hardcoded a Modifies 'val' parameter of spell effects that give bonuses by specified value. For example, Aenain makes Disrupting Ray decrease target's defense by additional 2 points: -```jsonc +```json "disruptingRay" : { "addInfo" : -2, "subtype" : "spell.disruptingRay", @@ -271,7 +271,7 @@ Modifies 'val' parameter of spell effects that give bonuses by specified value. Changes 'val' parameter of spell effects that give bonuses to a specified value. For example, Fortune cast by Melody always modifies luck by +3: -```jsonc +```json "fortune" : { "addInfo" : 3, "subtype" : "spell.fortune", @@ -346,8 +346,8 @@ Heroes affected by this bonus can not retreat or surrender in battle (Shackles o Negates all natural immunities for affected stacks. (Orb of Vulnerability) - subtype: - - immunityBattleWide: Entire battle will be affected by bonus - - immunityEnemyHero: Only enemy hero will be affected by bonus + - immunityBattleWide: Entire battle will be affected by bonus + - immunityEnemyHero: Only enemy hero will be affected by bonus ### OPENING_BATTLE_SPELL @@ -383,9 +383,9 @@ Increases movement speed of units in battle Increases base damage of creature in battle - subtype: - - creatureDamageMin: increases only minimal damage - - creatureDamageMax: increases only maximal damage - - creatureDamageBoth: increases both minimal and maximal damage + - creatureDamageMin: increases only minimal damage + - creatureDamageMax: increases only maximal damage + - creatureDamageBoth: increases both minimal and maximal damage - val: additional damage points ### SHOTS @@ -447,8 +447,8 @@ Affected units can not receive good or bad morale Affected unit can fly on the battlefield - subtype: - - movementFlying: creature will fly (slowly move across battlefield) - - movementTeleporting: creature will instantly teleport to destination, skipping movement animation. + - movementFlying: creature will fly (slowly move across battlefield) + - movementTeleporting: creature will instantly teleport to destination, skipping movement animation. ### SHOOTER @@ -525,19 +525,19 @@ Affected unit will ignore specified percentage of attacked unit defense (Behemot Affected units will receive reduced damage from attacks by other units - val: damage reduction, percentage -- subtype: - - damageTypeMelee: only melee damage will be reduced - - damageTypeRanged: only ranged damage will be reduced - - damageTypeAll: all damage will be reduced +- subtype: + - damageTypeMelee: only melee damage will be reduced + - damageTypeRanged: only ranged damage will be reduced + - damageTypeAll: all damage will be reduced ### PERCENTAGE_DAMAGE_BOOST Affected units will deal increased damage when attacking other units - val: damage increase, percentage -- subtype: - - damageTypeMelee: only melee damage will increased - - damageTypeRanged: only ranged damage will increased +- subtype: + - damageTypeMelee: only melee damage will increased + - damageTypeRanged: only ranged damage will increased ### GENERAL_ATTACK_REDUCTION @@ -576,18 +576,18 @@ Affected unit will never receive retaliations when attacking Affected unit will gain new creatures for each enemy killed by this unit - val: number of units gained per enemy killed -- subtype: - - soulStealPermanent: creature will stay after the battle - - soulStealBattle: creature will be lost after the battle +- subtype: + - soulStealPermanent: creature will stay after the battle + - soulStealBattle: creature will be lost after the battle ### TRANSMUTATION Affected units have chance to transform attacked unit to other creature type - val: chance for ability to trigger, percentage -- subtype: - - transmutationPerHealth: transformed unit will have same HP pool as original stack, - - transmutationPerUnit: transformed unit will have same number of units as original stack +- subtype: + - transmutationPerHealth: transformed unit will have same HP pool as original stack, + - transmutationPerUnit: transformed unit will have same number of units as original stack - addInfo: creature to transform to. If not set, creature will transform to same unit as attacker ### SUMMON_GUARDIANS @@ -613,10 +613,10 @@ Affected unit will attack units on all hexes that surround attacked hex Affected unit will retaliate before enemy attacks, if able -- subtype: - - damageTypeMelee: only melee attacks affected - - damageTypeRanged: only ranged attacks affected. Note that unit also requires ability to retaliate in ranged, such as RANGED_RETALIATION bonus - - damageTypeAll: any attacks are affected +- subtype: + - damageTypeMelee: only melee attacks affected + - damageTypeRanged: only ranged attacks affected. Note that unit also requires ability to retaliate in ranged, such as RANGED_RETALIATION bonus + - damageTypeAll: any attacks are affected ### SHOOTS_ALL_ADJACENT @@ -627,9 +627,9 @@ Affected unit will attack units on all hexes that surround attacked hex in range Affected unit will kills additional units after attack - val: chance to trigger, percentage -- subtype: - - destructionKillPercentage: kill percentage of units, - - destructionKillAmount: kill amount +- subtype: + - destructionKillPercentage: kill percentage of units, + - destructionKillAmount: kill amount - addInfo: amount or percentage to kill ### LIMITED_SHOOTING_RANGE @@ -669,6 +669,7 @@ Affected unit can attack walls during siege battles (Cyclops) ### CATAPULT_EXTRA_SHOTS Defines spell mastery level for spell used by CATAPULT bonus + - subtype: affected spell - val: spell mastery level to use @@ -760,18 +761,18 @@ Affected unit will deal additional damage after attack Affected unit will kill additional units after attack. Used for Death stare (Mighty Gorgon) ability and for Accurate Shot (Pirates, HotA) -- subtype: - - deathStareGorgon: only melee attack, random amount of killed units - - deathStareNoRangePenalty: only ranged attacks without obstacle (walls) or range penalty - - deathStareRangePenalty: only ranged attacks with range penalty - - deathStareObstaclePenalty: only ranged attacks with obstacle (walls) penalty - - deathStareRangeObstaclePenalty: only ranged attacks with both range and obstacle penalty - - deathStareCommander: fixed amount, both melee and ranged attacks -- val: - - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val - - for all other subtypes: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once, rounded up +- subtype: + - deathStareGorgon: only melee attack, random amount of killed units + - deathStareNoRangePenalty: only ranged attacks without obstacle (walls) or range penalty + - deathStareRangePenalty: only ranged attacks with range penalty + - deathStareObstaclePenalty: only ranged attacks with obstacle (walls) penalty + - deathStareRangeObstaclePenalty: only ranged attacks with both range and obstacle penalty + - deathStareCommander: fixed amount, both melee and ranged attacks +- val: + - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val + - for all other subtypes: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once, rounded up - addInfo: - - SpellID to be used as hit effect. If not set - 'deathStare' spell will be used. If set to "accurateShot" battle log messages will use alternative description + - SpellID to be used as hit effect. If not set - 'deathStare' spell will be used. If set to "accurateShot" battle log messages will use alternative description ### SPECIAL_CRYSTAL_GENERATION @@ -816,9 +817,9 @@ Determines how many times per combat affected creature can cast its targeted spe - subtype - spell id, eg. spell.iceBolt - value - chance (percent) - additional info - \[X, Y, Z\] - - X - spell mastery level (1 - Basic, 3 - Expert) - - Y = 0 - all attacks, 1 - shot only, 2 - melee only - - Z (optional) - layer for multiple SPELL_AFTER_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. + - X - spell mastery level (1 - Basic, 3 - Expert) + - Y = 0 - all attacks, 1 - shot only, 2 - melee only + - Z (optional) - layer for multiple SPELL_AFTER_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired. ### SPELL_BEFORE_ATTACK @@ -826,12 +827,12 @@ Determines how many times per combat affected creature can cast its targeted spe - subtype - spell id - value - chance % - additional info - \[X, Y, Z\] - - X - spell mastery level (1 - Basic, 3 - Expert) - - Y = 0 - all attacks, 1 - shot only, 2 - melee only - - Z (optional) - layer for multiple SPELL_BEFORE_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. + - X - spell mastery level (1 - Basic, 3 - Expert) + - Y = 0 - all attacks, 1 - shot only, 2 - melee only + - Z (optional) - layer for multiple SPELL_BEFORE_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired. -### SPECIFIC_SPELL_POWER +### SPECIFIC_SPELL_POWER - value: Used for Thunderbolt and Resurrection cast by units (multiplied by stack size). Also used for Healing secondary skill (for core:spell.firstAid used by First Aid tent) - subtype - spell id @@ -842,16 +843,16 @@ Determines how many times per combat affected creature can cast its targeted spe ### CREATURE_ENCHANT_POWER - - val: Total duration of spells cast by creature, in turns +- val: Total duration of spells cast by creature, in turns ### REBIRTH Affected stack will resurrect after death -- val - percent of total stack HP restored, not rounded. For instance, when 4 Phoenixes with Rebirth chance of 20% die, there is 80% chance than one Phoenix will rise. +- val - percent of total stack HP restored, not rounded. For instance, when 4 Phoenixes with Rebirth chance of 20% die, there is 80% chance than one Phoenix will rise. - subtype: - - rebirthRegular: Phoenix, as described above. - - rebirthSpecial: At least one unit will always rise (Sacred Phoenix) + - rebirthRegular: Phoenix, as described above. + - rebirthSpecial: At least one unit will always rise (Sacred Phoenix) ### ENCHANTED @@ -1000,9 +1001,9 @@ Affected heroes will be under effect of Visions spell, revealing information of - val: multiplier to effect range. Information is revealed within (val \* hero spell power) range - subtype: - - visionsMonsters: reveal information on monsters, - - visionsHeroes: reveal information on heroes, - - visionsTowns: reveal information on towns + - visionsMonsters: reveal information on monsters, + - visionsHeroes: reveal information on heroes, + - visionsTowns: reveal information on towns ### BLOCK_MAGIC_BELOW diff --git a/docs/modders/Bonus/Bonus_Updaters.md b/docs/modders/Bonus/Bonus_Updaters.md index f0d95f940..82b5dd2cb 100644 --- a/docs/modders/Bonus/Bonus_Updaters.md +++ b/docs/modders/Bonus/Bonus_Updaters.md @@ -10,45 +10,43 @@ Check the files in *config/heroes/* for additional usage examples. ## GROWS_WITH_LEVEL -- Type: Complex -- Parameters: valPer20, stepSize=1 -- Effect: Updates val to - -` ceil(valPer20 * floor(heroLevel / stepSize) / 20)` +- Type: Complex +- Parameters: valPer20, stepSize=1 +- Effect: Updates val to `ceil(valPer20 * floor(heroLevel / stepSize) / 20)` Example: The following updater will cause a bonus to grow by 6 for every 40 levels. At first level, rounding will cause the bonus to be 0. -` "updater" : {` -` "parameters" : [ 6, 2 ],` -` "type" : "GROWS_WITH_LEVEL"` -` }` +```json +"updater" : { + "parameters" : [ 6, 2 ], + "type" : "GROWS_WITH_LEVEL" +} +``` Example: The following updater will cause a bonus to grow by 3 for every 20 levels. At first level, rounding will cause the bonus to be 1. -` "updater" : {` -` "parameters" : [ 3 ],` -` "type" : "GROWS_WITH_LEVEL"` -` }` +```json +"updater" : { + "parameters" : [ 3 ], + "type" : "GROWS_WITH_LEVEL" +} +``` Remarks: -- The rounding rules are designed to match the attack/defense bonus +- The rounding rules are designed to match the attack/defense bonus progression for heroes with creature specialties in HMM3. -- There is no point in specifying val for a bonus with a +- There is no point in specifying val for a bonus with a GROWS_WITH_LEVEL updater. ## TIMES_HERO_LEVEL -- Type: Simple -- Effect: Updates val to +- Type: Simple +- Effect: Updates val to `val * heroLevel` -` val * heroLevel` - -Usage: - -` "updater" : "TIMES_HERO_LEVEL"` +Usage: `"updater" : "TIMES_HERO_LEVEL"` Remark: This updater is redundant, in the sense that GROWS_WITH_LEVEL can also express the desired scaling by setting valPer20 to 20\*val. It @@ -56,34 +54,30 @@ has been added for convenience. ## TIMES_STACK_LEVEL -- Type: Simple -- Effect: Updates val to - -` val * stackLevel` +- Type: Simple +- Effect: Updates val to `val * stackLevel` Usage: -` "updater" : "TIMES_STACK_LEVEL"` +`"updater" : "TIMES_STACK_LEVEL"` Remark: The stack level for war machines is 0. ## ARMY_MOVEMENT -- Type: Complex -- Parameters: basePerSpeed, dividePerSpeed, additionalMultiplier, - maxValue -- Effect: Updates val to val+= max((floor(basePerSpeed / - dividePerSpeed)\* additionalMultiplier), maxValue) -- Remark: this updater is designed for MOVEMENT bonus to match H3 army - movement rules (in the example - actual movement updater, which - produces values same as in default movement.txt). -- Example: +- Type: Complex +- Parameters: basePerSpeed, dividePerSpeed, additionalMultiplier, maxValue +- Effect: Updates val to `val+= max((floor(basePerSpeed / dividePerSpeed) * additionalMultiplier), maxValue)` +- Remark: this updater is designed for MOVEMENT bonus to match H3 army movement rules (in the example - actual movement updater, which produces values same as in default movement.txt). +- Example: -` "updater" : {` -` "parameters" : [ 20, 3, 10, 700 ],` -` "type" : "ARMY_MOVEMENT"` -` }` +```json +"updater" : { + "parameters" : [ 20, 3, 10, 700 ], + "type" : "ARMY_MOVEMENT" +} +``` ## BONUS_OWNER_UPDATER -TODO: document me \ No newline at end of file +TODO: document me diff --git a/docs/modders/Bonus/Bonus_Value_Types.md b/docs/modders/Bonus/Bonus_Value_Types.md index aca624516..9b4a6e75b 100644 --- a/docs/modders/Bonus/Bonus_Value_Types.md +++ b/docs/modders/Bonus/Bonus_Value_Types.md @@ -2,24 +2,29 @@ Total value of Bonus is calculated using the following: -- For each bonus source type we calculate new source value (for all bonus value types except PERCENT_TO_SOURCE and PERCENT_TO_TARGET_TYPE) using the following: -` newVal = (val * (100 + PERCENT_TO_SOURCE) / 100))` +- For each bonus source type we calculate new source value (for all bonus value types except PERCENT_TO_SOURCE and PERCENT_TO_TARGET_TYPE) using the following: + +```text +newVal = (val * (100 + PERCENT_TO_SOURCE) / 100)) +``` - PERCENT_TO_TARGET_TYPE applies as PERCENT_TO_SOURCE to targetSourceType of bonus. -- All bonus value types summarized and then used as subject of the following formula: +- All bonus value types summarized and then used as subject of the following formula: -` clamp(((BASE_NUMBER * (100 + PERCENT_TO_BASE) / 100) + ADDITIVE_VALUE) * (100 + PERCENT_TO_ALL) / 100), INDEPENDENT_MAX, INDEPENDENT_MIN)` +```text +clamp(((BASE_NUMBER * (100 + PERCENT_TO_BASE) / 100) + ADDITIVE_VALUE) * (100 + PERCENT_TO_ALL) / 100), INDEPENDENT_MAX, INDEPENDENT_MIN) +``` Semantics of INDEPENDENT_MAX and INDEPENDENT_MIN are wrapped, and first means than bonus total value will be at least INDEPENDENT_MAX, and second means than bonus value will be at most INDEPENDENT_MIN. ## List of all bonus value types -- ADDITIVE_VALUE -- BASE_NUMBER -- PERCENT_TO_ALL -- PERCENT_TO_BASE -- INDEPENDENT_MAX -- INDEPENDENT_MIN -- PERCENT_TO_SOURCE -- PERCENT_TO_TARGET_TYPE \ No newline at end of file +- ADDITIVE_VALUE +- BASE_NUMBER +- PERCENT_TO_ALL +- PERCENT_TO_BASE +- INDEPENDENT_MAX +- INDEPENDENT_MIN +- PERCENT_TO_SOURCE +- PERCENT_TO_TARGET_TYPE diff --git a/docs/modders/Bonus_Format.md b/docs/modders/Bonus_Format.md index 5dce1c1d0..be37fa558 100644 --- a/docs/modders/Bonus_Format.md +++ b/docs/modders/Bonus_Format.md @@ -4,7 +4,7 @@ All parameters but type are optional. -``` javascript +```json { // Type of the bonus. See Bonus Types for full list "type": "BONUS_TYPE", @@ -78,10 +78,10 @@ All parameters but type are optional. All string identifiers of items can be used in "subtype" field. This allows cross-referencing between the mods and make config file more readable. See [Game Identifiers](Game_Identifiers.md) for full list of available identifiers - + ### Example -``` javascript +```json "bonus" : { "type" : "HATE", @@ -90,4 +90,4 @@ See [Game Identifiers](Game_Identifiers.md) for full list of available identifie } ``` -This bonus makes creature do 50% more damage to Enchanters. \ No newline at end of file +This bonus makes creature do 50% more damage to Enchanters. diff --git a/docs/modders/Building_Bonuses.md b/docs/modders/Building_Bonuses.md index ac4e0c7d9..da815123b 100644 --- a/docs/modders/Building_Bonuses.md +++ b/docs/modders/Building_Bonuses.md @@ -12,17 +12,17 @@ should be moved to scripting. Includes: -- mystic pond -- treasury -- god of fire -- castle gates -- cover of darkness -- portal of summoning -- escape tunnel +- mystic pond +- treasury +- god of fire +- castle gates +- cover of darkness +- portal of summoning +- escape tunnel Function of all of these objects can be enabled by this: -``` javascript +```json "function" : "castleGates" ``` @@ -31,13 +31,13 @@ Function of all of these objects can be enabled by this: Hardcoded functionality for now due to complexity of these objects. Temporary can be handles as unique buildings. Includes: -- resource - resource -- resource - player -- artifact - resource -- resource - artifact -- creature - resource -- resource - skills -- creature - skeleton +- resource - resource +- resource - player +- artifact - resource +- resource - artifact +- creature - resource +- resource - skills +- creature - skeleton ### hero visitables @@ -46,10 +46,10 @@ handled via configurable objects system. Includes: -- gives mana points -- gives movement points -- give bonus to visitor -- permanent bonus to hero +- gives mana points +- gives movement points +- give bonus to visitor +- permanent bonus to hero ### generic functions @@ -58,31 +58,31 @@ CBuilding class. #### unlock guild level -``` javascript +```json "guildLevels" : 1 ``` #### unlock hero recruitment -``` javascript +```json "allowsHeroPurchase" : true ``` #### unlock ship purchase -``` javascript +```json "allowsShipPurchase" : true ``` #### unlock building purchase -``` javascript +```json "allowsBuildingPurchase" : true ``` #### unlocks creatures -``` javascript +```json "dwelling" : { "level" : 1, "creature" : "archer" } ``` @@ -92,31 +92,31 @@ Turn into town bonus? What about creature-specific bonuses from hordes? #### gives resources -``` javascript +```json "provides" : { "gold" : 500 } ``` #### gives guild spells -``` javascript +```json "guildSpells" : [5, 0, 0, 0, 0] ``` #### gives thieves guild -``` javascript +```json "thievesGuildLevels" : 1 ``` #### gives fortifications -``` javascript +```json "fortificationLevels" : 1 ``` #### gives war machine -``` javascript +```json "warMachine" : "ballista" ``` @@ -129,12 +129,12 @@ TODO: how to handle stackable bonuses like Necromancy Amplifier? Includes: -- bonus to defender -- bonus to alliance -- bonus to scouting range -- bonus to player +- bonus to defender +- bonus to alliance +- bonus to scouting range +- bonus to player -``` javascript +```json "bonuses" : { "moraleToDefenders" : @@ -162,12 +162,12 @@ Possible issue - with removing of fixed ID's buildings in different town may no longer share same ID. However Capitol must be unique across all town. Should be fixed somehow. -``` javascript +```json "onePerPlayer" : true ``` #### chance to be built on start -``` javascript +```json "prebuiltChance" : 75 -``` \ No newline at end of file +``` diff --git a/docs/modders/Campaign_Format.md b/docs/modders/Campaign_Format.md index 9fec41be2..9def306b1 100644 --- a/docs/modders/Campaign_Format.md +++ b/docs/modders/Campaign_Format.md @@ -3,12 +3,13 @@ ## Introduction Starting from version 1.3, VCMI supports its own campaign format. -Campaigns have *.vcmp file format and it consists from campaign json and set of scenarios (can be both *.vmap and *.h3m) +Campaigns have `*.vcmp` file format and it consists from campaign json and set of scenarios (can be both `*.vmap` and `*.h3m`) To start making campaign, create file named `header.json`. See also [Packing campaign](#packing-campaign) Basic structure of this file is here, each section is described in details below -```js + +```json { "version" : 1, @@ -32,7 +33,8 @@ Basic structure of this file is here, each section is described in details below ## Header properties In header are parameters describing campaign properties -```js + +```json ... "regions": {...}, "name": "Campaign name", @@ -60,7 +62,8 @@ In header are parameters describing campaign properties ## Scenario description Scenario description looks like follow: -```js + +```json { "map": "maps/SomeMap", "preconditions": [], @@ -77,7 +80,7 @@ Scenario description looks like follow: } ``` -- `"map"` map name without extension but with relative path. Both *.h3m and *.vmap maps are supported. If you will pack scenarios inside campaign, numerical map name should be used, see details in [packing campaign](#packing-campaign) +- `"map"` map name without extension but with relative path. Both `*.h3m` and `*.vmap` maps are supported. If you will pack scenarios inside campaign, numerical map name should be used, see details in [packing campaign](#packing-campaign) - `"preconditions"` enumerate scenarios indexes which must be completed to unlock this scenario. For example, if you want to make sequential missions, you should specify `"preconditions": []` for first scenario, but for second scenario it should be `"preconditions": [0]` and for third `"preconditions": [0, 1]`. But you can allow non-linear conquering using this parameter - `"color"` defines color id for the region. Possible values are `0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7` - `"difficulty"` sets initial difficulty for this scenario. If `"allowDifficultySelection"`is defined for campaign, difficulty may be changed by player. Possible values are `0: pawn, 1: knight, 2: rook, 3: queen, 4: king` @@ -96,7 +99,8 @@ Scenario description looks like follow: ### Prolog/Epilog Prolog and epilog properties are optional -```js + +```json { "video": "NEUTRALA.smk", //video to show "music": "musicFile.ogg", //music to play, should be located in music directory @@ -115,7 +119,7 @@ If `startOptions` is `none`, `bonuses` field will be ignored If `startOptions` is `bonus`, bonus format may vary depending on its type. -```js +```json { "what": "", @@ -158,19 +162,21 @@ If `startOptions` is `bonus`, bonus format may vary depending on its type. If `startOptions` is `crossover`, heroes from specific scenario will be moved to this scenario. Bonus format is following -```js +```json { "playerColor": 0, "scenario": 0 }, ``` + - `"playerColor"` from what player color heroes shall be taken. Possible values are `0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7` - `"scenario"` from which scenario heroes shall be taken. 0 means first scenario #### Hero start option If `startOptions` is `hero`, hero can be chosen as a starting bonus. Bonus format is following -```js + +```json { "playerColor": 0, "hero": "random" @@ -184,7 +190,7 @@ If `startOptions` is `hero`, hero can be chosen as a starting bonus. Bonus forma Predefined campaign regions are located in file `campaign_regions.json` -```js +```json { "background": "ownRegionBackground.png", "suffix": ["Enabled", "Selected", "Conquered"], @@ -201,7 +207,7 @@ Predefined campaign regions are located in file `campaign_regions.json` - `"background"` optional - use own image name for background instead of adding "_BG" to the prefix as name - `"prefix"` used to identify all images related to campaign. In this example (if background parameter wouldn't exists), background picture will be `G3_BG` - `"suffix"` optional - use other suffixes than the default `En`, `Se` and `Co` for the three different images -- `"infix"` used to identify all images related to region. In this example, it will be pictures whose files names begin with `G3A_..., G3B_..., G3C_..."` +- `"infix"` used to identify all images related to region. In this example, it will be pictures whose files names begin with `G3A_..., G3B_..., G3C_..."` - `"labelPos"` optional - to add scenario name as label on map - `"colorSuffixLength"` identifies suffix length for region colourful frames. 0 is no color suffix (no colorisation), 1 is used for `R, B, N, G, O, V, T, P`, value 2 is used for `Re, Bl, Br, Gr, Or, Vi, Te, Pi` @@ -213,6 +219,7 @@ This file is a zip archive. The scenarios should be named as in `"map"` field from header. Subfolders are allowed. ## Compatibility table + | Version | Min VCMI | Max VCMI | Description | |---------|----------|----------|-------------| -| 1 | 1.3 | | Initial release | \ No newline at end of file +| 1 | 1.3 | | Initial release | diff --git a/docs/modders/Configurable_Widgets.md b/docs/modders/Configurable_Widgets.md index 883b249b1..2d968af49 100644 --- a/docs/modders/Configurable_Widgets.md +++ b/docs/modders/Configurable_Widgets.md @@ -21,7 +21,8 @@ In this tutorial we will recreate options tab to support chess timers UI. ### Creating mod structure To start making mod, create following folders structure; -``` + +```text extendedLobby/ |- content/ | |- sprites/ @@ -31,6 +32,7 @@ extendedLobby/ ``` File `mod.json` is generic and could look like this: + ```json { "name" : "Configurable UI tutorial mod", @@ -44,7 +46,7 @@ File `mod.json` is generic and could look like this: } ``` -After that you can copy `extendedLobby/ folder to `mods/` folder and your mod will immediately appear in launcher but it does nothing for now. +After that you can copy `extendedLobby/` folder to `mods/` folder and your mod will immediately appear in launcher but it does nothing for now. ### Making layout for timer @@ -64,6 +66,7 @@ So we need to modify turn duration label and add combo box with timer types Open `optionsTab.json` and scroll it until you see comment `timer`. Three elements after this comment are related to timer. Let's find first element, which is label + ```json { "items" @@ -85,6 +88,7 @@ Let's find first element, which is label ``` And modify it a bit + ```json { "name": "labelTimer", //add name, only for convenience @@ -98,6 +102,7 @@ And modify it a bit ``` But we also need proper background image for this label. Add image widget BEFORE labelTimer widget: + ```json { "type": "picture", @@ -109,6 +114,7 @@ But we also need proper background image for this label. Add image widget BEFORE ... }, ``` + In order to make it work, add file `RmgTTBk.bmp` to `content/sprites/` Elements named `labelTurnDurationValue` and `sliderTurnDuration` we will keep without change - they are needed to configure classic timer. @@ -272,6 +278,7 @@ After view part is done, let's make behavioural part. Let's hide elements, related to classic timer when chess timer is selected and show them back if classic selected. To do that, find `"variables"` part inside `optionsTab.json` and add there `"timers"` array, containing 2 elements: + ```json "variables": { @@ -300,7 +307,7 @@ Now we show and hide elements, but visually you still can some "artifacts": Снимок экрана 2023-08-30 в 15 51 22 -It's because options tab background image we use has those elements drawn. Let's hide them with overlay image `timchebk.bmp`. +It's because options tab background image we use has those elements drawn. Let's hide them with overlay image `timchebk.bmp`. It should be drawn before all other timer elements: ```json @@ -328,8 +335,9 @@ It works and can switch elements, the only missing part is chess timer configura We should add text input fields, to specify different timers. We will use background for them `timerField.bmp`, copy it to `content/sprites/` folder of your mod. -There are 4 different timers: base, turn, battle and creature. Read about them here: https://github.com/vcmi/vcmi/issues/1364 +There are 4 different timers: base, turn, battle and creature. Read about them here: We can add editors for them into items list, their format will be following: + ```json { "name": "chessFieldBase", @@ -345,6 +353,7 @@ We can add editors for them into items list, their format will be following: ``` Add three remaining elements for different timers by yourself. You can play with all settings, except callback. There are 4 predefined callbacks to setup timers: + - `parseAndSetTimer_base` - `parseAndSetTimer_turn` - `parseAndSetTimer_battle` @@ -363,7 +372,7 @@ There are different basic types, which can be used as value. #### Primitive types -Read JSON documentation for primitive types description: https://www.json.org/json-en.html +Read JSON documentation for primitive types description: #### Text @@ -404,7 +413,7 @@ Predefined fonts: Hint text is a pair of strings, one is usually shown in status bar when cursor hovers element, another hint while right button pressed. Each of elements is a [Text](#text) -``` +```json { "hover": "Text", "help": "Text @@ -430,6 +439,7 @@ One of predefined values: ### Configurable objects Configurable object has following structure: + ```json { "items": [], @@ -621,7 +631,7 @@ Filling area with texture `"color"`: [color](#color), -`"text": string` optional, default text. Translations are not supported +`"text": string` optional, default text. Translations are not supported `"position"`: [position](#position) @@ -748,11 +758,13 @@ Used only as special object for [combo box](#combo-box) `"position"`: [position](#position) `"items": []` array of overlay widgets with certain types and names: - - `"name": "hoverImage"`, `"type": ` [picture](#picture) - image to be shown when cursor hovers elements - - `"name": "labelName"`, `"type": ` [label](#label) - element caption + +- `"name": "hoverImage"`, `"type":` [picture](#picture) - image to be shown when cursor hovers elements +- `"name": "labelName"`, `"type":` [label](#label) - element caption **Callbacks** - - `sliderMove` connect to slider callback to correctly navigate over elements + +- `sliderMove` connect to slider callback to correctly navigate over elements #### Layout @@ -769,7 +781,8 @@ Used only as special object for [combo box](#combo-box) While designing a new element, you can make it configurable to reuse all functionality described above. It will provide flexibility to further changes as well as modding capabilities. Class should inherit `InterfaceObjectConfigurable`. -```C++ + +```cpp #include "gui/InterfaceObjectConfigurable.h" //assuming we are in client folder class MyYesNoDialog: public InterfaceObjectConfigurable @@ -781,7 +794,7 @@ class MyYesNoDialog: public InterfaceObjectConfigurable To make new object work, it's sufficient to define constructor, which receives const reference to `JsonNode`. -```C++ +```cpp MyYesNoDialog::MyYesNoDialog(const JsonNode & config): InterfaceObjectConfigurable(), //you can pass arguments same as for CIntObject { @@ -808,13 +821,13 @@ MyYesNoDialog::MyYesNoDialog(const JsonNode & config): You can build custom widgets, related to your UI element specifically. Like in example above, there is Item widget, which can be also used on JSON config. -```C++ +```cpp REGISTER_BUILDER("myItem", &MyYesNoDialog::buildMyItem); ``` You have to define function, which takes JsonNode as an argument and return pointer to built widget -```C++ +```cpp std::shared_ptr MyYesNoDialog::buildMyItem(const JsonNode & config) { auto position = readPosition(config["position"]); @@ -840,7 +853,7 @@ After that, if your JSON file has items with type "MyItem", the new Item element After calling `build(config)` variables defined in config JSON file become available. You can interpret them and use in callbacks or in element code -```C++ +```cpp build(config); if(variables["colorfulText"].Bool()) diff --git a/docs/modders/Difficulty.md b/docs/modders/Difficulty.md index 91f78af9c..b071f4da4 100644 --- a/docs/modders/Difficulty.md +++ b/docs/modders/Difficulty.md @@ -7,7 +7,7 @@ Difficulty configuration is located in [config/difficulty.json](../config/diffic ## Format summary -``` javascript +```json { "human": //parameters impacting human players only { @@ -50,7 +50,7 @@ For both types of bonuses, `source` should be specified as `OTHER`. ## Example -```js +```json { //will give 150% extra health to all players' creatures if specified in "battleBonuses" array "type" : "STACK_HEALTH", "val" : 150, @@ -63,4 +63,4 @@ For both types of bonuses, `source` should be specified as `OTHER`. ## Compatibility Starting from VCMI 1.4 `startres.json` is not available anymore and will be ignored if present in any mod. -Thus, `Resourceful AI` mod of version 1.2 won't work anymore. \ No newline at end of file +Thus, `Resourceful AI` mod of version 1.2 won't work anymore. diff --git a/docs/modders/Entities_Format/Artifact_Format.md b/docs/modders/Entities_Format/Artifact_Format.md index 83b1adca2..9c0e2c71d 100644 --- a/docs/modders/Entities_Format/Artifact_Format.md +++ b/docs/modders/Entities_Format/Artifact_Format.md @@ -6,13 +6,13 @@ Artifact bonuses use [Bonus Format](../Bonus_Format.md) In order to make functional artifact you also need: -- Icon for hero inventory (1 image) -- Icon for popup windows (1 image, optional) -- Animation for adventure map (1 animation) +- Icon for hero inventory (1 image) +- Icon for popup windows (1 image, optional) +- Animation for adventure map (1 animation) ## Format -``` jsonc +```json { // Type of this artifact - creature, hero or commander "type": ["HERO", "CREATURE", "COMMANDER"] diff --git a/docs/modders/Entities_Format/Battle_Obstacle_Format.md b/docs/modders/Entities_Format/Battle_Obstacle_Format.md index e9b00d3d6..84b8184ce 100644 --- a/docs/modders/Entities_Format/Battle_Obstacle_Format.md +++ b/docs/modders/Entities_Format/Battle_Obstacle_Format.md @@ -1,6 +1,6 @@ # Battle Obstacle Format -```jsonc +```json // List of terrains on which this obstacle can be used "allowedTerrains" : [] @@ -24,4 +24,4 @@ // If set to true, obstacle will appear in front of units or other battlefield objects "foreground" : false -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Battlefield_Format.md b/docs/modders/Entities_Format/Battlefield_Format.md index 21c219273..7797b02ec 100644 --- a/docs/modders/Entities_Format/Battlefield_Format.md +++ b/docs/modders/Entities_Format/Battlefield_Format.md @@ -1,6 +1,6 @@ # Battlefield Format -```jsonc +```json // Human-readable name of the battlefield "name" : "", @@ -22,4 +22,4 @@ // List of battle hexes that will be always blocked on this battlefield (e.g. ship to ship battles) "impassableHexes" : [ 10, 20, 50 ], -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Biome_Format.md b/docs/modders/Entities_Format/Biome_Format.md index a0d2a3689..7c2d76e5c 100644 --- a/docs/modders/Entities_Format/Biome_Format.md +++ b/docs/modders/Entities_Format/Biome_Format.md @@ -8,7 +8,7 @@ The purpose is to create visually attractive and consistent maps, which will als If not enough biomes are defined for [terrain type](Terrain_Format.md), map generator will fall back to using all available templates that match this terrain, which was original behavior before 1.5.0. -``` json +```json "obstacleSetId" : { "biome" : { "terrain" : "grass", // Id or vector of Ids this obstacle set can spawn at @@ -38,5 +38,3 @@ Currently algorithm picks randomly: - One or two sets of **rocks** (small objects) - One of each remaining types of object (**structure**, **animal**, **other**), until enough number of sets is picked. - Obstacles marked as **other** are picked last, and are generally rare. - - diff --git a/docs/modders/Entities_Format/Creature_Format.md b/docs/modders/Entities_Format/Creature_Format.md index 252a5e140..66cb5a83d 100644 --- a/docs/modders/Entities_Format/Creature_Format.md +++ b/docs/modders/Entities_Format/Creature_Format.md @@ -8,22 +8,22 @@ In order to make functional creature you also need: ### Animation -- Battle animation (1 def file) -- Set of rendered projectiles (1 def files, shooters only) -- Adventure map animation (1 def file) +- Battle animation (1 def file) +- Set of rendered projectiles (1 def files, shooters only) +- Adventure map animation (1 def file) ### Images -- Small portrait for hero exchange window (1 image) -- Large portrait for hero window (1 image) +- Small portrait for hero exchange window (1 image) +- Large portrait for hero window (1 image) ### Sounds -- Set of sounds (up to 8 sounds) +- Set of sounds (up to 8 sounds) ## Format -``` javascript +```json // camelCase unique creature identifier "creatureName" : { @@ -217,4 +217,4 @@ In order to make functional creature you also need: ... ] } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Creature_Help.md b/docs/modders/Entities_Format/Creature_Help.md index d5ca1a590..a2234e40b 100644 --- a/docs/modders/Entities_Format/Creature_Help.md +++ b/docs/modders/Entities_Format/Creature_Help.md @@ -5,6 +5,7 @@ This page helps you to create a creature (i.e. a unit that fights in a battle) f ## Utilities You need to download the two utilities [`DefPreview`](https://sourceforge.net/projects/grayface-misc/files/DefPreview-1.2.1/) and [`H3DefTool`](https://sourceforge.net/projects/grayface-misc/files/H3DefTool-3.4.2/) from the internet: + - `DefPreview` converts a `.def` file to `.bmp` images - `H3DefTool` converts `.bmp` images to a `.def` file @@ -21,7 +22,9 @@ The sun is always at zenith, so the shadow is always behind. The reason is that We don't know the right elevation angle for the view. ### 3D render -You can render your creature using a 3D software like _Blender_. You can start with those free-licenced rigged 3D models: + +You can render your creature using a 3D software like *Blender*. You can start with those free-licenced rigged 3D models: + - [Fantasy-bandit](https://www.cgtrader.com/free-3d-models/character/man/fantasy-bandit) - [Monster-4](https://www.cgtrader.com/free-3d-models/character/fantasy-character/monster-4-f5757b92-dc9c-4f5e-ad0d-593203d14fe2) - [Crypt-fiend-modular-character](https://www.cgtrader.com/free-3d-models/character/fantasy-character/crypt-fiend-modular-character-demo-scene) @@ -33,57 +36,60 @@ You can render your creature using a 3D software like _Blender_. You can start w - [Shani](https://www.cgtrader.com/free-3d-models/character/woman/shani-3d-character) You can also create your 3D model from a single image: -- _Stable Fast 3D_: https://huggingface.co/spaces/stabilityai/stable-fast-3d -- _Unique3D_: https://huggingface.co/spaces/abreza/Unique3D -To use it in _Blender_, create a `.blend` project and import the file. To render the texture: -1. Add a _Principled BSDF_ material to the object -1. Create a _Color Attribute_ in the _Shader Editor_ view -1. Link the Color output of the _Color Attribute_ to the _Base color_ input of the _Principled BSDF_ +- *Stable Fast 3D*: +- *Unique3D*: -You can improve details by cropping the source image on a detail and generate a model for this detail. Once both imported in _Blender_, melt them together. +To use it in *Blender*, create a `.blend` project and import the file. To render the texture: -Render the images without background by selecting png RVBA and disabling background (_Film_ -> _Filter_ -> _Transparent_). It avoids the creatures to have an ugly dark border. Then, to correctly separate the creature from the cyan area, in _GIMP_, apply the threeshold on the transparency by clicking on _Layer_ -> _Transparency_ -> _Alpha threeshold_. +1. Add a *Principled BSDF* material to the object +1. Create a *Color Attribute* in the *Shader Editor* view +1. Link the Color output of the *Color Attribute* to the *Base color* input of the *Principled BSDF* + +You can improve details by cropping the source image on a detail and generate a model for this detail. Once both imported in *Blender*, melt them together. + +Render the images without background by selecting png RVBA and disabling background (*Film* -> *Filter* -> *Transparent*). It avoids the creatures to have an ugly dark border. Then, to correctly separate the creature from the cyan area, in *GIMP*, apply the threeshold on the transparency by clicking on *Layer* -> *Transparency* -> *Alpha threeshold*. The global FPS of the game is 10 f/s but you can render at a higher level and configure it in the `.json` files. We are not in the 1990's. ### IA render -You can also use an AI like _Flux_ to generate the main creature representation: https://huggingface.co/spaces/multimodalart/FLUX.1-merged +You can also use an AI like *Flux* to generate the main creature representation: -Then you can add random animations for idle states with _SVD_: https://huggingface.co/spaces/xi0v/Stable-Video-Diffusion-Img2Vid +Then you can add random animations for idle states with *SVD*: -Most of the time, the creatures do not move more than one pixel in an idle animation. The reason may be to avoid too much animation on screen and make the transition with the other animations always seamless. Use poses with _ControlNet_ or _OpenPose_. For specific animations, I recommend to use _Cinemo_ because it adds a description prompt but the resolution is smaller: https://huggingface.co/spaces/maxin-cn/Cinemo +Most of the time, the creatures do not move more than one pixel in an idle animation. The reason may be to avoid too much animation on screen and make the transition with the other animations always seamless. Use poses with *ControlNet* or *OpenPose*. For specific animations, I recommend to use *Cinemo* because it adds a description prompt but the resolution is smaller: -Make animations seamless from one to another. To do this, you can draw the first and the last images with a prompt with _ToonCrafter_: https://huggingface.co/spaces/ChristianHappy/tooncrafter +Make animations seamless from one to another. To do this, you can draw the first and the last images with a prompt with *ToonCrafter*: -Most of the time, you need to increase the resolution or the quality of your template image, so use _SUPIR_: https://huggingface.co/spaces/Fabrice-TIERCELIN/SUPIR +Most of the time, you need to increase the resolution or the quality of your template image, so use *SUPIR*: ## Battle sound effect -To create the audio effects, I recommend to use _Tango 2_: https://huggingface.co/spaces/declare-lab/tango2 +To create the audio effects, I recommend to use *Tango 2*: -The quality is better than _Stable Audio_. +The quality is better than *Stable Audio*. ## Map render We don't know the right elevation angle for the view but 45° elevation seems to be a good choice. For the sunlight direction, I would say 45° elevation and 45° azimut. -The map creatures are not rendered on the map with vanishing points but in isometric. You can [get an orthogonal render in Blender](https://blender.stackexchange.com/a/135384/2768). If you are creating a creature and its updated version, most of the time, the both creatures are not oriented to the same side on the map. I think that the animation on the map is usually the _Mouse Over_ animation on battle. +The map creatures are not rendered on the map with vanishing points but in isometric. You can [get an orthogonal render in Blender](https://blender.stackexchange.com/a/135384/2768). If you are creating a creature and its updated version, most of the time, the both creatures are not oriented to the same side on the map. I think that the animation on the map is usually the *Mouse Over* animation on battle. -You can see that the view angle is higher than on a battle. To change the angle from a battle sprite, you can use _Zero 1-to-3_: https://huggingface.co/spaces/cvlab/zero123-live +You can see that the view angle is higher than on a battle. To change the angle from a battle sprite, you can use *Zero 1-to-3*: -You can get higher resolution using this Video AI that can control the motion of the camera: https://huggingface.co/spaces/TencentARC/MotionCtrl_SVD +You can get higher resolution using this Video AI that can control the motion of the camera: -If you have a 3D software, you can get better quality by converting your image into 3D model and then render it from another angle using _Stable Fast 3D_: https://huggingface.co/spaces/stabilityai/stable-fast-3d +If you have a 3D software, you can get better quality by converting your image into 3D model and then render it from another angle using *Stable Fast 3D*: -Follow this comment to retrieve the color: https://huggingface.co/stabilityai/TripoSR/discussions/1#65e8a8e5e214f37d85dad366 +Follow this comment to retrieve the color: ### Shadow render There are no strong rules in the original game about the angle of the shadows on the map. Different buildings have inconsistent shadows. To draw the shadow, I recommend the following technique: Let's consider that the object is a vertical cone: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | @@ -98,6 +104,7 @@ Let's consider that the object is a vertical cone: | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | Locate the top and its projection to the ground: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | @@ -112,6 +119,7 @@ Locate the top and its projection to the ground: | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | Then draw a rectangle triangle on the left: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | @@ -126,6 +134,7 @@ Then draw a rectangle triangle on the left: | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | The square top is the projection of the shadow of the top of the cone: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | @@ -140,6 +149,7 @@ The square top is the projection of the shadow of the top of the cone: | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | Then you can draw the rest of the shadow: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | diff --git a/docs/modders/Entities_Format/Faction_Format.md b/docs/modders/Entities_Format/Faction_Format.md index 739086331..8b4b01431 100644 --- a/docs/modders/Entities_Format/Faction_Format.md +++ b/docs/modders/Entities_Format/Faction_Format.md @@ -8,46 +8,46 @@ In order to make functional town, you also need: ### Images -- Creature backgrounds images, 120x100 and 130x100 versions (2 images) -- Set of puzzle map pieces (48 images) -- Background scenery (1 image) -- Mage guild window view (1 image) -- Town hall background (1 image) +- Creature backgrounds images, 120x100 and 130x100 versions (2 images) +- Set of puzzle map pieces (48 images) +- Background scenery (1 image) +- Mage guild window view (1 image) +- Town hall background (1 image) -- Set of town icons, consists from all possible combinations of: (8 +- Set of town icons, consists from all possible combinations of: (8 images total) - - small and big icons - - village and fort icons - - built and normal icons + - small and big icons + - village and fort icons + - built and normal icons -- Set for castle siege screen, consists from: - - Background (1 image) - - Destructible towers (3 parts, 3 images each) - - Destructible walls (4 parts, 3 images each) - - Static walls (3 images) - - Town gates (5 images) - - Moat (2 images) +- Set for castle siege screen, consists from: + - Background (1 image) + - Destructible towers (3 parts, 3 images each) + - Destructible walls (4 parts, 3 images each) + - Static walls (3 images) + - Town gates (5 images) + - Moat (2 images) ### Animation -- Adventure map images for village, town and capitol (3 def files) +- Adventure map images for village, town and capitol (3 def files) ### Music -- Town theme music track (at least 1 music file) +- Town theme music track (at least 1 music file) ### Buildings Each town requires a set of buildings (Around 30-45 buildings) -- Town animation file (1 animation file) -- Selection highlight (1 image) -- Selection area (1 image) -- Town hall icon (1 image) +- Town animation file (1 animation file) +- Selection highlight (1 image) +- Selection area (1 image) +- Town hall icon (1 image) ## Faction node (root entry for town configuration) -```jsonc +```json // Unique faction identifier. "myFaction" : { @@ -108,7 +108,7 @@ Each town requires a set of buildings (Around 30-45 buildings) ## Town node -```jsonc +```json { // Field that describes behavior of map object part of town. Town-specific part of object format "mapObject" : @@ -256,7 +256,7 @@ Each town requires a set of buildings (Around 30-45 buildings) ## Siege node -```jsonc +```json // Describes town siege screen // Comments in the end of each graphic position indicate specify required suffix for image // Note: one not included image is battlefield background with suffix "BACK" @@ -340,7 +340,9 @@ Each town requires a set of buildings (Around 30-45 buildings) ``` ## Building node + See [Town Building Format](Town_Building_Format.md) ## Structure node -See [Town Building Format](Town_Building_Format.md) \ No newline at end of file + +See [Town Building Format](Town_Building_Format.md) diff --git a/docs/modders/Entities_Format/Faction_Help.md b/docs/modders/Entities_Format/Faction_Help.md index c141a8673..ebbcd38fa 100644 --- a/docs/modders/Entities_Format/Faction_Help.md +++ b/docs/modders/Entities_Format/Faction_Help.md @@ -3,90 +3,109 @@ This page helps you to create from scratch a VCMI mod that adds a new faction. The faction mod structure is described [here](Faction_Format.md). ## Questioning the faction creation + Before creating a faction, be aware that creating a faction mod is lots of work. You can start [creating creatures](Creature_Help.md) in a creature mod that can be converted into a faction mod after. This way, you are sure to release something. The smallest contribution is a hero portrait that you can suggest on an existing mod. You can also restore the former version of the [Ruins faction](https://github.com/vcmi-mods/ruins-town/tree/1bea30a1d915770e2fd0f95d158030815ff462cd). You would only have to remake the similar parts to the new version. ## Make a playable faction mod -Before creating your content, retrieve the content of an existing faction mod like [Highlands town](https://github.com/vcmi-mods/highlands-town). To download the project, click on the _Code_ button and click on _Download ZIP_. The first thing to do is to change all the faction identifiers in the files following the [faction format](Faction_Format.md) and manage to play with the faction and the original without any conflict. To play to a faction, you have to add all the files in your _Mods_ folder. When it works, you will be able to modify the content step by step. + +Before creating your content, retrieve the content of an existing faction mod like [Highlands town](https://github.com/vcmi-mods/highlands-town). To download the project, click on the *Code* button and click on *Download ZIP*. The first thing to do is to change all the faction identifiers in the files following the [faction format](Faction_Format.md) and manage to play with the faction and the original without any conflict. To play to a faction, you have to add all the files in your *Mods* folder. When it works, you will be able to modify the content step by step. Keep in mind that the most important part of a faction mod, above the animations, the graphisms and the musics, is the concept because if you have to change it, you have to change everything else. All the remaining content can be improved by the community. ## Town screen -### Background -Beware to direct all the shadows to the same direction. The easiest way to create the background is to use a text-to-image AI. The free most powerful AI at the moment is _Flux_ available here: https://huggingface.co/spaces/multimodalart/FLUX.1-merged -In the _Advanced Options_, set the width to 800px and set the height to 374px. +### Background + +Beware to direct all the shadows to the same direction. The easiest way to create the background is to use a text-to-image AI. The free most powerful AI at the moment is *Flux* available here: + +In the *Advanced Options*, set the width to 800px and set the height to 374px. ### Buildings -To render a building upon the background, I recommend to use an inpainting AI like _BRIA Inpaint_: https://huggingface.co/spaces/briaai/BRIA-2.3-Inpainting + +To render a building upon the background, I recommend to use an inpainting AI like *BRIA Inpaint*: The idea is to select the area where you want to add the building. As a prompt, describe the new building. The advantage is a perfect match between the background and the building. Keep in mind that to correctly integrate a building image, it must contain the image of the background on its edges. It simulates the semi-transparency. -You can also animate the building or the background using _Stable Video Diffusion_: https://huggingface.co/spaces/multimodalart/stable-video-diffusion +You can also animate the building or the background using *Stable Video Diffusion*: ## Map dwellings -You may want to get the same render as in the town, so you have to change the angle and the shadows. If you handle a 3D model software, you can start with _Stable Fast 3D_: https://huggingface.co/spaces/stabilityai/stable-fast-3d + +You may want to get the same render as in the town, so you have to change the angle and the shadows. If you handle a 3D model software, you can start with *Stable Fast 3D*: The map dwellings are not rendered on the map with vanishing points but in isometric. You can [get an orthogonal render in Blender](https://blender.stackexchange.com/a/135384/2768). -Without 3D, you can use _Zero 1-to-3_: https://huggingface.co/spaces/cvlab/zero123-live +Without 3D, you can use *Zero 1-to-3*: -You can get higher resolution using this Video AI that can control the motion of the camera: https://huggingface.co/spaces/TencentARC/MotionCtrl_SVD +You can get higher resolution using this Video AI that can control the motion of the camera: -The buildings on the map are more satured than on town screen. If you have to reduce the size of an image, do not use interpolation (LANCZOS, Bilinear...) to get more details, not a blurred image. If you need to increase the resolution or the quality of your template image, use _SUPIR_: https://huggingface.co/spaces/Fabrice-TIERCELIN/SUPIR +The buildings on the map are more satured than on town screen. If you have to reduce the size of an image, do not use interpolation (LANCZOS, Bilinear...) to get more details, not a blurred image. If you need to increase the resolution or the quality of your template image, use *SUPIR*: ## Map buildings + The AIs badly understand the sun direction and the perspective angles. To generate the buildings on the adventure map: 1. Open the HOMM3 map editor 1. Put items all around a big empty area 1. Make a screenshot -1. Go on an AI like _BRIA Inpaint_: https://huggingface.co/spaces/briaai/BRIA-2.3-Inpainting +1. Go on an AI like *BRIA Inpaint*: 1. Inpaint the (big) empty middle with the brush 1. Use a prompt like: `A dark house, at the center of the image, map, isometric, parallel perspective, sunlight from the bottom right` ## Music + Here are unused available themes: * [Synthetic Horizon](https://github.com/Fabrice-TIERCELIN/forge/raw/theme/content/music/factions/theme.ogg) + 1. Prompt: `Dystopy, Cinematic classical, Science fiction, 160 bpm, Best quality, Futuristic` -1. Initially created for: _Forge town_ +1. Initially created for: *Forge town* * [Quantum Overture](https://github.com/Fabrice-TIERCELIN/asylum-town/raw/theme/asylum-town/content/Music/factions/AsylumTown.ogg) + 1. Prompt: `Clef shifting, Fantasy, Mystical, Overworldly, Cinematic classical` -1. Initially created for: _Asylum town_ +1. Initially created for: *Asylum town* * [Warrior s March](https://github.com/vcmi-mods/ruins-town/assets/20668759/964f27de-6feb-4ef6-9d25-455f52938cef) + 1. Prompt: `Powerful percussions, Drums, Battle Anthem, Rythm, Warrior, 160 bpm, Celtic, New age, Instrumental, Accoustic, Medieval` -1. Initially created for: _Ruins town_ +1. Initially created for: *Ruins town* * [Clan of Echoes](https://github.com/Fabrice-TIERCELIN/ruins-town/raw/theme/ruins-town/content/music/ruins.ogg) + 1. Prompt: `new age, medieval, celtic, warrior, battle, soundtrack, accoustic, drums, rythm` -1. Initially created for: _Ruins town_ +1. Initially created for: *Ruins town* * [Enchanted Reverie](https://github.com/Fabrice-TIERCELIN/grove/raw/theme/Grove/content/Music/factions/GroveTown.ogg) + 1. Prompt: `Classical music, Soundtrack, Score, Instrumental, 160 bpm, ((((fantasy)))), mystic` -1. Initially created for: _Grove town_ +1. Initially created for: *Grove town* * [World Discovery](https://github.com/vcmi-mods/asylum-town/assets/20668759/34438523-8a44-44ca-b493-127501b474a6) + 1. Prompt: `Clef shifting, fantasy, mystical, overworldly, Cinematic classical` -1. Initially created for: _Asylum town_ +1. Initially created for: *Asylum town* * [Enchanted Ballad](https://github.com/vcmi-mods/fairy-town/assets/20668759/619e6e33-d940-4899-8c76-9c1e8d3d20aa) + 1. Prompt: `Females vocalize, Cinematic classical, Harp, Fairy tale, Princess, 160 bpm` -1. Initially created for: _Fairy town_ +1. Initially created for: *Fairy town* * [Baroque Resurgence](https://github.com/Fabrice-TIERCELIN/courtyard_proposal/raw/theme/Courtyard/Content/music/factions/courtyard/CourtTown.ogg) + 1. Prompt: `Baroque, Instrumental, 160 bpm, Cinematic classical, Best quality` -1. Initially created for: _Courtyard town_ +1. Initially created for: *Courtyard town* * [Harvest Parade](https://github.com/Fabrice-TIERCELIN/greenhouse-town/raw/theme/Greenhouse/content/Music/town.ogg) -1. Prompt: `Marching band, Best quality, Happy, Vegetables` -1. Initially created for: _Green town_ -Those themes have been generated using _[Udio](https://udio.com)_. +1. Prompt: `Marching band, Best quality, Happy, Vegetables` +1. Initially created for: *Green town* + +Those themes have been generated using *[Udio](https://udio.com)*. ## Screenshots + Most of the time, the first screenshot is the townscreen because it's the most specific content. ## Recycle -Some mods contain neutral heroes or creatures. You can integrate them in your faction mod. Don't forget to remove the content from the original mod. \ No newline at end of file + +Some mods contain neutral heroes or creatures. You can integrate them in your faction mod. Don't forget to remove the content from the original mod. diff --git a/docs/modders/Entities_Format/Hero_Class_Format.md b/docs/modders/Entities_Format/Hero_Class_Format.md index 68833fa5d..1a25cc01c 100644 --- a/docs/modders/Entities_Format/Hero_Class_Format.md +++ b/docs/modders/Entities_Format/Hero_Class_Format.md @@ -4,12 +4,12 @@ In order to make functional hero class you also need: -- Adventure animation (1 def file) -- Battle animation, male and female version (2 def files) +- Adventure animation (1 def file) +- Battle animation, male and female version (2 def files) ## Format -``` javascript +```json // Unique identifier of hero class, camelCase "myClassName" : { @@ -106,4 +106,4 @@ In order to make functional hero class you also need: "conflux" : 6 } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Hero_Type_Format.md b/docs/modders/Entities_Format/Hero_Type_Format.md index b646c0e89..1028bd86f 100644 --- a/docs/modders/Entities_Format/Hero_Type_Format.md +++ b/docs/modders/Entities_Format/Hero_Type_Format.md @@ -4,12 +4,12 @@ In order to make functional hero you also need: -- Portraits, small and big versions (2 images) -- Specialty icons, small and big versions (2 images) +- Portraits, small and big versions (2 images) +- Specialty icons, small and big versions (2 images) ## Format -``` javascript +```json "myHeroName" : { // Identifier of class this hero belongs to. Such as knight or battleMage @@ -133,4 +133,4 @@ In order to make functional hero you also need: "creature" : "griffin" } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/River_Format.md b/docs/modders/Entities_Format/River_Format.md index 35635768d..7730cf750 100644 --- a/docs/modders/Entities_Format/River_Format.md +++ b/docs/modders/Entities_Format/River_Format.md @@ -2,7 +2,7 @@ ## Format -```jsonc +```json "newRiver" : { // Two-letters unique identifier for this river. Used in map format @@ -28,4 +28,4 @@ ... ] } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Road_Format.md b/docs/modders/Entities_Format/Road_Format.md index d78de9b10..df48ade5e 100644 --- a/docs/modders/Entities_Format/Road_Format.md +++ b/docs/modders/Entities_Format/Road_Format.md @@ -2,7 +2,7 @@ ## Format -```jsonc +```json "newRoad" : { // Two-letters unique identifier for this road. Used in map format @@ -17,4 +17,4 @@ // How many movement points needed to move hero "moveCost" : 66 } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Secondary_Skill_Format.md b/docs/modders/Entities_Format/Secondary_Skill_Format.md index 367ca775f..d5be72bd0 100644 --- a/docs/modders/Entities_Format/Secondary_Skill_Format.md +++ b/docs/modders/Entities_Format/Secondary_Skill_Format.md @@ -2,7 +2,16 @@ ## Main format -```jsonc +```json +{ + // Skill be only be available on maps with water + "onlyOnWaterMap" : false, + // Skill is not available on maps at random + "special" : true +} +``` + +```json { "skillName": { @@ -46,7 +55,7 @@ level fields become optional if they equal "base" configuration. ## Skill level format -```jsonc +```json { // Localizable description // Use {xxx} for formatting @@ -78,7 +87,7 @@ level fields become optional if they equal "base" configuration. The following modifies the tactics skill to grant an additional speed boost at advanced and expert levels. -```jsonc +```json "core:tactics" : { "base" : { "effects" : { @@ -114,4 +123,4 @@ boost at advanced and expert levels. } } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Spell_Format.md b/docs/modders/Entities_Format/Spell_Format.md index e0910ca09..9aa39403f 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/docs/modders/Entities_Format/Spell_Format.md @@ -2,7 +2,7 @@ ## Main format -``` javascript +```json { "spellName": { @@ -156,7 +156,7 @@ TODO -``` javascript +```json { "projectile": [ {"minimumAngle": 0 ,"defName":"C20SPX4"}, @@ -179,7 +179,7 @@ Json object with data common for all levels can be put here. These configuration This will make spell affect single target on all levels except expert, where it is massive spell. -``` javascript +```json "base":{ "range": 0 }, @@ -192,7 +192,7 @@ This will make spell affect single target on all levels except expert, where it TODO -``` javascript +```json { //Mandatory, localizable description. Use {xxx} for formatting @@ -262,7 +262,7 @@ Configurable spells ignore *offensive* flag, *effects* and *cumulativeEffects*. TODO -``` javascript +```json "mod:effectId":{ @@ -283,7 +283,7 @@ TODO TODO -``` javascript +```json "mod:effectId":{ @@ -304,7 +304,7 @@ TODO Configurable version of Clone spell. -``` javascript +```json "mod:effectId":{ @@ -320,7 +320,7 @@ TODO If effect is automatic, spell behave like offensive spell (uses power, levelPower etc) -``` javascript +```json "mod:effectId":{ @@ -368,7 +368,7 @@ TODO If effect is automatic, spell behave like \[de\]buff spell (effect and cumulativeEffects ignored) -``` javascript +```json "mod:effectId":{ @@ -389,21 +389,21 @@ cumulativeEffects ignored) TODO - CREATURE target (only battle spells) - - range 0: smart assumed single creature target - - range "X" + smart modifier = enchanter casting, expert massive spells - - range "X" + no smart modifier = armageddon, death ripple, destroy undead - - any other range (including chain effect) - - smart modifier: smth like cloud of confusion in H4 (if I remember correctly :) ) - - no smart modifier: like inferno, fireball etc. but target only creature +- range 0: smart assumed single creature target +- range "X" + smart modifier = enchanter casting, expert massive spells +- range "X" + no smart modifier = armageddon, death ripple, destroy undead +- any other range (including chain effect) +- smart modifier: smth like cloud of confusion in H4 (if I remember correctly :) ) +- no smart modifier: like inferno, fireball etc. but target only creature - NO_TARGET - - no target selection,(abilities, most adventure spells) +- no target selection,(abilities, most adventure spells) - LOCATION - - any tile on map/battlefield (inferno, fireball etc.), DD also here but with special handling - - clearTarget - destination hex must be clear (unused so far) - - clearAfffected - all affected hexes must be clear (forceField, fireWall) +- any tile on map/battlefield (inferno, fireball etc.), DD also here but with special handling +- clearTarget - destination hex must be clear (unused so far) +- clearAfffected - all affected hexes must be clear (forceField, fireWall) - OBSTACLE target - - range 0: any single obstacle - - range X: all obstacles \ No newline at end of file +- range 0: any single obstacle +- range X: all obstacles diff --git a/docs/modders/Entities_Format/Terrain_Format.md b/docs/modders/Entities_Format/Terrain_Format.md index d12a7f7cf..e0a0fff43 100644 --- a/docs/modders/Entities_Format/Terrain_Format.md +++ b/docs/modders/Entities_Format/Terrain_Format.md @@ -2,7 +2,7 @@ ## Format -```jsonc +```json "newTerrain" : { // Two-letters unique identifier for this terrain. Used in map format @@ -75,4 +75,4 @@ "terrainViewPatterns" : "", } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Town_Building_Format.md b/docs/modders/Entities_Format/Town_Building_Format.md index 67079fda4..c97b87c7b 100644 --- a/docs/modders/Entities_Format/Town_Building_Format.md +++ b/docs/modders/Entities_Format/Town_Building_Format.md @@ -4,17 +4,20 @@ Each building requires following assets: -- Town animation file (1 animation file) -- Selection highlight (1 image) -- Selection area (1 image) -- Town hall icon (1 image) +- Town animation file (1 animation file) +- Selection highlight (1 image) +- Selection area (1 image) +- Town hall icon (1 image) ## Examples + These are just a couple of examples of what can be done in VCMI. See vcmi configuration files to check how buildings from Heroes III are implemented or other mods for more examples + #### -##### Order of Fire from Inferno: -```jsonc +##### Order of Fire from Inferno + +```json "special4": { "requires" : [ "mageGuild1" ], "name" : "Order of Fire", @@ -34,10 +37,11 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ] } } -``` +``` ##### Mana Vortex from Dungeon -```jsonc + +```json "special2": { "requires" : [ "mageGuild1" ], "name" : "Mana Vortex", @@ -65,7 +69,8 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ``` #### Resource Silo with custom production -```jsonc + +```json "resourceSilo": { "name" : "Wood Resource Silo", "description" : "Produces 2 wood every day", @@ -80,7 +85,8 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ``` #### Brotherhood of Sword - bonuses in siege -```jsonc + +```json "special3": { // replaces +1 Morale bonus from Tavern "upgradeReplacesBonuses" : true, @@ -96,7 +102,8 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ``` #### Lighthouse - bonus to all heroes under player control -```jsonc + +```json "special1": { "bonuses": [ { @@ -112,7 +119,7 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ## Town Building node -```jsonc +```json { // Numeric identifier of this building "id" : 0, @@ -211,7 +218,7 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config Building requirements can be described using logical expressions: -```jsonc +```json "requires" : [ "allOf", // Normal H3 "build all" mode @@ -228,10 +235,13 @@ Building requirements can be described using logical expressions: ] ] ``` + ### List of unique town buildings #### Buildings from Heroes III + Following Heroes III buildings can be used as unique buildings for a town. Their functionality should be identical to a corresponding H3 building. H3 buildings that are not present in this list contain no hardcoded functionality. See vcmi json configuration to see how such buildings can be implemented in a mod. + - `mysticPond` - `artifactMerchant` - `freelancersGuild` @@ -244,16 +254,18 @@ Following Heroes III buildings can be used as unique buildings for a town. Their - `treasury` #### Buildings from other Heroes III mods + Following HotA buildings can be used as unique building for a town. Functionality should match corresponding HotA building: + - `bank` #### Custom buildings -In addition to above, it is possible to use same format as [Rewardable](../Map_Objects/Rewardable.md) map objects for town buildings. In order to do that, configuration of a rewardable object must be placed into `configuration` json node in building config. +In addition to above, it is possible to use same format as [Rewardable](../Map_Objects/Rewardable.md) map objects for town buildings. In order to do that, configuration of a rewardable object must be placed into `configuration` json node in building config. ### Town Structure node -```jsonc +```json { // Main animation file for this building "animation" : "", @@ -281,16 +293,18 @@ In addition to above, it is possible to use same format as [Rewardable](../Map_O } ``` - #### Markets in towns + Market buildings require list of available [modes](../Map_Objects/Market.md) ##### Marketplace -```jsonc + +```json "marketplace": { "marketModes" : ["resource-resource", "resource-player"] }, ``` ##### Artifact merchant -```jsonc + +```json "special1": { "type" : "artifactMerchant", "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] }, -``` \ No newline at end of file +``` diff --git a/docs/modders/File_Formats.md b/docs/modders/File_Formats.md index b80188625..900c90a25 100644 --- a/docs/modders/File_Formats.md +++ b/docs/modders/File_Formats.md @@ -1,12 +1,13 @@ # File Formats -This page describes which file formats are supported by vcmi. +This page describes which file formats are supported by vcmi. In most cases, VCMI supports formats that were supported by Heroes III, with addition of new formats that are more convenient to use without specialized tools. See categories below for more details on specific formats ### Images For images VCMI supports: + - png. Recommended for usage in mods - bmp. While this format is supported, bmp images have no compressions leading to large file sizes - pcx (h3 version). Note that this is format that is specific to Heroes III and has nothing in common with widely known .pcx format. Files in this format generally can only be found inside of .lod archive of Heroes III and are usually extracted as .bmp files @@ -26,6 +27,7 @@ For animations VCMI supports .def format from Heroes III as well as alternative ### Sounds For sounds VCMI currently supports: + - .ogg/vorbis format - preferred for mods. Unlike wav, vorbis uses compression which may cause some data loss, however even 128kbit is generally undistinguishable from lossless formats - .wav format. This is format used by H3. It is supported by vcmi, but it may result in large file sizes (and as result - large mods) @@ -36,6 +38,7 @@ Support for additional formats, such as ogg/opus or flac may be added in future ### Music For music VCMI currently supports: + - .ogg/vorbis format - preferred for mods. Generally offers better quality and lower sizes compared to mp3 - .mp3 format. This is format used by H3 @@ -51,6 +54,7 @@ Starting from VCMI 1.6, following video container formats are supported by VCMI: - .webm - modern, free format that is recommended for modding. Supported video codecs: + - bink and smacker - formats used by Heroes III, should be used only to avoid re-encoding - theora - used by Heroes III: HD Edition - vp8 - modern format with way better compression compared to formats used by Heroes III @@ -59,6 +63,7 @@ Supported video codecs: Support for av1 video codec is likely to be added in future. Supported audio codecs: + - binkaudio and smackaud - formats used by Heroes III - vorbis - modern format with good compression level - opus - recommended, improvement over vorbis. Any bitrate is supported, with 128 kbit probably being the best option diff --git a/docs/modders/Game_Identifiers.md b/docs/modders/Game_Identifiers.md index 4e212fe01..24e95b530 100644 --- a/docs/modders/Game_Identifiers.md +++ b/docs/modders/Game_Identifiers.md @@ -493,7 +493,7 @@ This is a list of all game identifiers available to modders. Note that only iden - hero.thorgrim - hero.thunar - hero.tiva -- hero.torosar +- hero.torosar - hero.tyraxor - hero.tyris - hero.ufretin diff --git a/docs/modders/HD_Graphics.md b/docs/modders/HD_Graphics.md index fc0f155e8..c5a8d3d2a 100644 --- a/docs/modders/HD_Graphics.md +++ b/docs/modders/HD_Graphics.md @@ -12,11 +12,13 @@ If user for example selects 3x resolution and only 2x exists in mod then the 2x ## Mod -For upscaled images you have to use following folders (next to `sprites` and `data` folders): +For upscaled images you have to use following folders (next to `sprites`, `data` and `video` folders): + - `sprites2x`, `sprites3x`, `sprites4x` for sprites - `data2x`, `data3x`, `data4x` for images +- `video2x`, `video3x`, `video4x` for videos -The sprites should have the same name and folder structure as in `sprites` and `data` folder. All images that are missing in the upscaled folders are scaled with the selected upscaling filter instead of using prescaled images. +The sprites should have the same name and folder structure as in `sprites`, `data` and `video` folder. All images that are missing in the upscaled folders are scaled with the selected upscaling filter instead of using prescaled images. ### Shadows / Overlays @@ -24,11 +26,13 @@ It's also possible (but not necessary) to add high-definition shadows: Just plac In future, such shadows will likely become required to correctly exclude shadow from effects such as Clone spell. Shadow images are used only for animations of following objects: + - All adventure map objects - All creature animations in combat Same for overlays with `-overlay`. But overlays are **necessary** for some animation graphics. They will be colorized by VCMI. Currently needed for: + - Flaggable adventure map objects. Overlay must contain a transparent image with white flags on it and will be used to colorize flags to owning player -- Creature battle animations, idle and mouse hover group. Overlay must contain a transparent image with white outline of creature for highlighting on mouse hover) +- Creature battle animations, idle and mouse hover group. Overlay must contain a transparent image with white outline of creature for highlighting on mouse hover diff --git a/docs/modders/Map_Editor.md b/docs/modders/Map_Editor.md index 3c472d8a3..ceb6853a4 100644 --- a/docs/modders/Map_Editor.md +++ b/docs/modders/Map_Editor.md @@ -38,7 +38,7 @@ Templates are dynamically filtered depending on parameters you choose. To load the map, press open and select map file from the browser. -You can load both *.h3m and *.vmap formats but for saving *.vmap is allowed only. +You can load both *.h3m and*.vmap formats but for saving *.vmap is allowed only. ## Views @@ -60,7 +60,7 @@ There are 3 buttons switching views Снимок экра
 
 <img width= -# Setup terrain +## Setup terrain 1. Select brush you want @@ -77,7 +77,7 @@ There are 3 buttons switching views Снимок экра
 
 #### Drawing roads and rivers
 
-Actually, the process to draw rivers or roads is exactly the same as for terrains. You need to select tiles and then choose road/river type from the panel. 
+Actually, the process to draw rivers or roads is exactly the same as for terrains. You need to select tiles and then choose road/river type from the panel.
 
 <img width= @@ -85,9 +85,10 @@ To erase roads or rivers, you need to select tiles to be cleaned and press empty -_Erasing works either for roads or for rivers, e.g. empty button from the roads tab erases roads only, but not rivers. You also can safely select bigger area, because it won't erase anything on tiles without roads/rivers accordingly_ +*Erasing works either for roads or for rivers, e.g. empty button from the roads tab erases roads only, but not rivers. You also can safely select bigger area, because it won't erase anything on tiles without roads/rivers accordingly* ### About brushes + * Buttons "1", "2", "4" - 1x1, 2x2, 4x4 brush sizes accordingly * Button "[]" - non-additive rectangle selection * Button "O" - lasso brush (not implemented yet) @@ -171,7 +172,7 @@ You can modify general properties of the map ## Player settings -Open **Map** menu on the top and select **Player settings" +Open **Map** menu on the top and select **Player settings** @@ -208,6 +209,7 @@ vcmieditor loads set of mods using exactly same mechanism as game uses and mod m The mods mechanism used in map editor is the same as in game. To enable or disable mods + * Start launcher, activate or deactivate mods you want * Close launcher * Run map editor @@ -233,4 +235,4 @@ You also may have other mods being activated in addition to what was used during #### Mod versions -In the future, the will be support of mods versioning so map will contain information about mods used and game can automatically search and activate required mods or let user know which are required. However, it's not implemented yet \ No newline at end of file +In the future, the will be support of mods versioning so map will contain information about mods used and game can automatically search and activate required mods or let user know which are required. However, it's not implemented yet diff --git a/docs/modders/Map_Object_Format.md b/docs/modders/Map_Object_Format.md index 5f6472e6c..f487c60f4 100644 --- a/docs/modders/Map_Object_Format.md +++ b/docs/modders/Map_Object_Format.md @@ -4,11 +4,11 @@ Full object consists from 3 parts: -- Object group - set of objects that have similar behavior and share +- Object group - set of objects that have similar behavior and share same identifier in H3 (towns, heroes, mines, etc) -- Object type - object with fixed behavior but without fixed +- Object type - object with fixed behavior but without fixed appearance. Multiple objects types may share same group -- Object template - defines appearance of an object - image used to +- Object template - defines appearance of an object - image used to display it, its size & blockmap. These entries only describe templates that will be used when object is placed via map editor or generated by the game. When new object is created its starting @@ -16,7 +16,7 @@ Full object consists from 3 parts: ## Object group format -``` javascript +```json { "myCoolObjectGroup": @@ -42,6 +42,7 @@ Full object consists from 3 parts: ## Object types ### Moddable types + These are object types that are available for modding and have configurable properties - `configurable` - see [Rewardable](Map_Objects/Rewardable.md). Visitable object which grants all kinds of rewards (gold, experience, Bonuses etc...) @@ -55,6 +56,7 @@ These are object types that are available for modding and have configurable prop - `terrain` - Defines terrain overlays such as magic grounds. TODO: documentation. See config files in vcmi installation for reference ### Common types + These are types that don't have configurable properties, however it is possible to add additional map templates for this objects, for use in editor or in random maps generator - `static` - Defines unpassable static map obstacles that can be used by RMG @@ -79,6 +81,7 @@ These are types that don't have configurable properties, however it is possible - `monolith` ### Internal types + These are internal types that are generally not available for modding and are handled by vcmi internally. - `hero` @@ -96,7 +99,7 @@ These are internal types that are generally not available for modding and are ha ## Object type format -``` javascript +```json { "myCoolObject": { @@ -150,7 +153,7 @@ These are internal types that are generally not available for modding and are ha ## Object template format -``` javascript +```json { "myCoolObjectTemplate" : { diff --git a/docs/modders/Map_Objects/Boat.md b/docs/modders/Map_Objects/Boat.md index 8d67581a5..4c462ed38 100644 --- a/docs/modders/Map_Objects/Boat.md +++ b/docs/modders/Map_Objects/Boat.md @@ -1,6 +1,6 @@ # Boat -``` javascript +```json { // Layer on which this boat moves. Possible values: // "land" - same rules as movement of hero on land @@ -27,4 +27,4 @@ // List of bonuses that will be granted to hero located in the boat "bonuses" : { BONUS_FORMAT } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Creature_Bank.md b/docs/modders/Map_Objects/Creature_Bank.md index e5389846b..76a6c647e 100644 --- a/docs/modders/Map_Objects/Creature_Bank.md +++ b/docs/modders/Map_Objects/Creature_Bank.md @@ -6,9 +6,11 @@ Format of rewards is same as in [Rewardable Objects](Rewardable.md) Deprecated in 1.6. Please use [Rewardable Objects](Rewardable.md) instead. See Conversion from 1.5 format section below for help with migration ### Example + This example defines a rewardable object with functionality similar of H3 creature bank. See [Rewardable Objects](Rewardable.md) for detailed documentation of these properties. -```jsonc + +```json { "name" : "Cyclops Stockpile", @@ -93,6 +95,7 @@ See [Rewardable Objects](Rewardable.md) for detailed documentation of these prop ``` ### Conversion from 1.5 format + This is a list of changes that needs to be done to bank config to migrate it to 1.6 system. See [Rewardable Objects](Rewardable.md) documentation for description of new fields - If your object type has defined `handler`, change its value from `bank` to `configurable` @@ -112,7 +115,7 @@ This is a list of changes that needs to be done to bank config to migrate it to ### Old format (1.5 or earlier) -``` jsonc +```json { /// If true, battle setup will be like normal - Attacking player on the left, enemy on the right "regularUnitPlacement" : true, diff --git a/docs/modders/Map_Objects/Dwelling.md b/docs/modders/Map_Objects/Dwelling.md index df369c502..7ca9aaf57 100644 --- a/docs/modders/Map_Objects/Dwelling.md +++ b/docs/modders/Map_Objects/Dwelling.md @@ -1,6 +1,6 @@ # Dwelling -``` javascript +```json { /// List of creatures in this bank. Each list represents one "level" of bank /// Creatures on the same level will have shared growth and available number (similar to towns) @@ -19,4 +19,4 @@ { "amount" : 12, "type" : "earthElemental" } ] } -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Flaggable.md b/docs/modders/Map_Objects/Flaggable.md index f11bfca79..10acde5e3 100644 --- a/docs/modders/Map_Objects/Flaggable.md +++ b/docs/modders/Map_Objects/Flaggable.md @@ -3,12 +3,13 @@ Flaggable object are those that can be captured by a visiting hero. H3 examples are mines, dwellings, or lighthouse. Currently, it is possible to make flaggable objects that provide player with: + - Any [Bonus](Bonus_Format.md) supported by bonus system - Daily resources income (wood, ore, gold, etc) ## Format description -```jsonc +```json { "baseObjectName" : { "name" : "Object name", diff --git a/docs/modders/Map_Objects/Market.md b/docs/modders/Map_Objects/Market.md index 2b411dfb8..dc24ecb9f 100644 --- a/docs/modders/Map_Objects/Market.md +++ b/docs/modders/Map_Objects/Market.md @@ -7,7 +7,7 @@ Markets can be added as any other object with special handler called "market". Here is schema describing such object -```js +```json "seafaringAcademy" : //object name { "handler" : "market", //market handler @@ -34,6 +34,7 @@ Here is schema describing such object Mode parameter defines a way to exchange different entities. Multiple modes can be specified to support several types of exchange. Following options are supported: + * `"resource-resource"` - regular resource exchange, like trading post * `"resource-player"` - allows to send resources to another player * `"creature-resource"` - acts like freelance guild @@ -49,6 +50,7 @@ Following options are supported: ### Trading post Trading post allows to exchange resources and send resources to another player, so it shall be configured this way: + ```json "modes" : ["resource-resource", "resource-player"] ``` @@ -83,14 +85,14 @@ See [Secondary skills](Rewardable.md#secondary-skills) description for more deta ### Example for University of magic (e.g conflux building) -```js +```json "modes" : ["resource-skill"], "offer" : ["airMagic", "waterMagic", "earthMagic", "fireMagic"] ``` ### Example for regular University -```js +```json "modes" : ["resource-skill"], "offer" : [ //4 random skills except necromancy { "noneOf" : ["necromancy"] }, @@ -98,4 +100,4 @@ See [Secondary skills](Rewardable.md#secondary-skills) description for more deta { "noneOf" : ["necromancy"] }, { "noneOf" : ["necromancy"] } ] -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index d7b1761d3..aa9224e92 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -1,8 +1,10 @@ # Rewardable ## Base object definition + Rewardable object is defined similarly to other objects, with key difference being `handler`. This field must be set to `"handler" : "configurable"` in order for vcmi to use this mode. -```jsonc + +```json { "baseObjectName" : { "name" : "Object name", @@ -34,7 +36,8 @@ Rewardable object is defined similarly to other objects, with key difference bei ``` ## Configurable object definition -```jsonc + +```json // List of potential rewards "rewards" : [ { @@ -173,6 +176,7 @@ This property allows defining "variables" that are shared between all rewards an Variables are randomized only once, so you can use them multiple times for example, to give skill only if hero does not have this skill (e.g. Witch Hut). Example of creation of a variable named "gainedSkill" of type "secondarySkill": + ```json "variables" : { "secondarySkill" : { @@ -187,6 +191,7 @@ Example of creation of a variable named "gainedSkill" of type "secondarySkill": ``` Possible variable types: + - number: can be used in any place that expects a number - artifact - spell @@ -194,6 +199,7 @@ Possible variable types: - secondarySkill To reference variable in limiter prepend variable name with '@' symbol: + ```json "secondary" : { "@gainedSkill" : 1 @@ -201,12 +207,14 @@ To reference variable in limiter prepend variable name with '@' symbol: ``` ## Reset Parameters definition + This property describes how object state should be reset. Objects without this field will never reset its state. + - Period describes interval between object resets in day. Periods are counted from game start and not from hero visit, so reset duration of 7 will always reset object on new week & duration of 28 will always reset on new month. - If `visitors` is set to true, game will reset list of visitors (heroes and players) on start of new period, allowing revisits of objects with `visitMode` set to `once`, `hero`, or `player`. Objects with visit mode set to `bonus` are not affected. In order to allow revisit such objects use appropriate bonus duration (e.g. `ONE_DAY` or `ONE_WEEK`) instead. - If `rewards` is set to true, object will re-randomize its provided rewards, similar to such H3 objects as "Fountain of Fortune" or "Windmill" -```jsonc +```json "resetParameters" : { "period" : 7, "visitors" : true, @@ -215,15 +223,17 @@ This property describes how object state should be reset. Objects without this f ``` ## Appear Chance definition + This property describes chance for reward to be selected. When object is initialized on map load, game will roll a "dice" - random number in range 0-99, and pick all awards that have appear chance within selected number. Note that object that uses appearChance MUST have continuous range for every value in 0-99 range. For example, object with 3 different rewards may want to define them as + - `"min" : 0, "max" : 33` - `"min" : 33, "max" : 66` - `"min" : 66, "max" : 100` In other words, min chance of second reward must be equal to max chance of previous reward -```jsonc +```json "appearChance": { // (Advanced) rewards with different dice number will get different dice number @@ -240,42 +250,47 @@ In other words, min chance of second reward must be equal to max chance of previ ``` ## Configurable Properties + Unless stated othervice, all numbers in this section can be replaced with random values, e.g. -```jsonc + +```json "minLevel" : { "min" : 5, "max" : 10 } // select random number between 5-10, including both 5 & 10 "minLevel" : [ 2, 4, 6, 8, 10] // (VCMI 1.2) select random number out of provided list, with equal chance for each ``` -In this case, actual value for minLevel will be picked randomly. +In this case, actual value for minLevel will be picked randomly. Keep in mind, that all randomization is performed on map load and on object reset (if `rewards` field in `resetParameter` was set). ### Current Day + - Can only be used as limiter. To pass, current day of week should be equal to this value. 1 = first day of the week, 7 = last day -```jsonc +```json "dayOfWeek" : 0 ``` - Can only be used as limiter. To pass, number of days since game started must be at equal or greater than this value -```jsonc +```json "daysPassed" : 8 ``` ### Resource + - Can be used as limiter. To pass, player needs to have specified resources. Note that limiter will NOT take resources. - Can be used as reward to grant resources to player - If negative value is used as reward, it will be used as cost and take resources from player -```jsonc +```json "resources": { "crystal" : 6, "gold" : -1000, }, ``` + - Alternative format that allows random selection of a resource type -```jsonc +```json "resources": [ { "anyOf" : [ "wood", "ore" ], @@ -289,68 +304,75 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Experience + - Can be used as limiter - Can be used as reward to grant experience to hero -```jsonc +```json "heroExperience" : 1000, ``` ### Hero Level + - Can be used as limiter. Hero requires to have at least specified level - Can be used as reward, will grant hero experience amount equal to the difference between the hero's next level and current level (Tree of Knowledge) -```jsonc +```json "heroLevel" : 1, ``` ### Mana Points + - Can be used as limiter. Hero must have at least specific mana amount - Can be used as reward, to give mana points to hero. Mana points may go above mana pool limit. - If negative value is used as reward, it will be used as cost and take mana from player -```jsonc +```json "manaPoints": -10, ``` - If giving mana points puts hero above mana pool limit, any overflow will be multiplied by specified percentage. If set to 0, mana will not go above mana pool limit. -```jsonc +```json "manaOverflowFactor" : 50, ``` ### Mana Percentage + - Can be used as limiter. Hero must have at least specific mana percentage - Can be used to set hero mana level to specified percentage value, not restricted to mana pool limit (Magic Well, Mana Spring) -```jsonc +```json "manaPercentage": 200, ``` ### Movement Points + - Can NOT be used as limiter - Can be used as reward, to give movement points to hero. Movement points may go above mana pool limit. -```jsonc +```json "movePoints": 200, ``` - + ### Movement Percentage + - Can NOT be used as limiter - Can be used to set hero movement points level to specified percentage value. Value of 0 will take away any remaining movement points -```jsonc +```json "movePercentage": 50, ``` ### Primary Skills + - Can be used as limiter, hero must have primary skill at least at specified level - Can be used as reward, to increase hero primary skills by selected value - If reward value is negative, value will be used as cost, decreasing primary skill - Each primary skill can be explicitly specified or randomly selected - Possible values: `"attack", "defence", "spellpower", "knowledge"` -```jsonc +```json "primary": [ { // Specific primary skill @@ -376,13 +398,15 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Secondary Skills + - Can be used as limiter, hero must have secondary skill at least at specified level - Can be used as reward, to grant secondary skills to hero - If hero already has specified skill, the skills will be leveled up specified number of times - If hero does not have selected skill and have free skill slots, he will receive skill at specified level - Possible values: 1 (basic), 2 (advanced), 3 (expert) - Each secondary skill can be explicitly specified or randomly selected -```jsonc + +```json "secondary": [ { // Specific skill @@ -416,6 +440,7 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Bonus System + - Can be used as reward, to grant bonus to player - if present, MORALE and LUCK bonus will add corresponding image component to UI. - Note that unlike most values, parameter of bonuses can NOT be randomized @@ -433,11 +458,12 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Artifacts + - Can be used as limiter, hero must have artifact either equipped or in backpack - Can be used as reward, to give new artifact to a hero - Artifacts added as reward will be used for text substitution. First `%s` in text string will be replaced with name of an artifact -```jsonc +```json "artifacts": [ "ribCage" ], @@ -447,7 +473,7 @@ Keep in mind, that all randomization is performed on map load and on object rese - For artifact class possible values are "TREASURE", "MINOR", "MAJOR", "RELIC" - Artifact value range can be specified with min value and max value -```jsonc +```json "artifacts": [ { "class" : "TREASURE", @@ -458,11 +484,12 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Spells + - Can be used as limiter - Can be used as reward, to give new spell to a hero - Spells added as reward will be used for text substitution. First `%s` in text string will be replaced with spell name -```jsonc +```json "spells": [ "magicArrow" ], @@ -471,7 +498,7 @@ Keep in mind, that all randomization is performed on map load and on object rese - Alternative format, random spell selection - Spell can be selected from specifically selected school -```jsonc +```json "spells": [ { "level" : 1, @@ -485,7 +512,7 @@ Keep in mind, that all randomization is performed on map load and on object rese - Can be used as limiter. Hero must be able to learn spell to pass the limiter - Hero is considered to not able to learn the spell if: - - he already has specified spell -- - he does not have a spellbook +- - he does not have a spellbook - - he does not have sufficient Wisdom level for this spell ```json @@ -495,11 +522,13 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Creatures + - Can be used as limiter - Can be used as reward, to give new creatures to a hero - If hero does not have enough free slots, game will show selection dialog to pick troops to keep - It is possible to specify probability to receive upgraded creature -```jsonc + +```json "creatures" : [ { "type" : "archer", @@ -510,13 +539,15 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Guards + - When used in a reward, these creatures will be added to guards of the objects - Hero must defeat all guards before being able to receive rewards - Guards are only reset when object rewards are reset - Requires `guardsLayout` property to be set in main part of object configuration - It is possible to add up to 7 slots of creatures - Guards of the same creature type will never merge or rearrange their stacks -```jsonc + +```json "guards" : [ { "type" : "archer", "amount" : 20 }, { "type" : "archer", "amount" : 20, "upgradeChance" : 30 }, @@ -525,17 +556,19 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Creatures Change + - Can NOT be used as limiter - Can be used as reward, to replace creatures in hero army. It is possible to use this parameter both for upgrades of creatures as well as for changing them into completely unrelated creature, e.g. similar to Skeleton Transformer - This parameter will not change creatures given by `creatures` parameter on the same visit -```jsonc +```json "changeCreatures" : { "cavalier" : "champion" } ``` ### Spell cast + - Can NOT be used as limiter - As reward, instantly casts adventure map spell for visiting hero. All checks for spell book, wisdom or presence of mana will be ignored. It's possible to specify school level at which spell will be casted. If it's necessary to reduce player's mana or do some checks, they shall be introduced as limiters and other rewards - School level possible values: 1 (basic), 2 (advanced), 3 (expert) @@ -567,28 +600,31 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Player color + - Can be used as limiter - Can NOT be used as reward - Only players with specific color can pass the limiter -```jsonc +```json "colors" : [ "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" ] ``` ### Hero types + - Can be used as limiter - Can NOT be used as reward - Only specific heroes can pass the limiter -```jsonc +```json "heroes" : [ "orrin" ] ``` ### Hero classes + - Can be used as limiter - Can NOT be used as reward - Only heroes belonging to specific classes can pass the limiter -```jsonc +```json "heroClasses" : [ "battlemage" ] -``` \ No newline at end of file +``` diff --git a/docs/modders/Mod_File_Format.md b/docs/modders/Mod_File_Format.md index 8a689ccf8..aabb3a59f 100644 --- a/docs/modders/Mod_File_Format.md +++ b/docs/modders/Mod_File_Format.md @@ -2,7 +2,7 @@ ## Fields with description of mod -``` javascript +```json { // Name of your mod. While it does not have hard length limit // it should not be longer than ~30 symbols to fit into allowed space @@ -91,7 +91,7 @@ These are fields that are present only in local mod.json file -``` javascript +```json { // Following section describes configuration files with content added by mod @@ -195,7 +195,7 @@ These are fields that are present only in local mod.json file In addition to field listed above, it is possible to add following block for any language supported by VCMI. If such block is present, Launcher will use this information for displaying translated mod information and game will use provided json files to translate mod to specified language. See [Translations](Translations.md) for more information -``` +```json "" : { "name" : "", "description" : "", @@ -210,7 +210,7 @@ See [Translations](Translations.md) for more information These are fields that are present only in remote repository and are generally not used in mod.json -```jsonc +```json { // URL to mod.json that describes this mod "mod" : "https://raw.githubusercontent.com/vcmi-mods/vcmi-extras/vcmi-1.4/mod.json", @@ -228,4 +228,4 @@ These are fields that are present only in remote repository and are generally no For mod description it is possible to use certain subset of HTML as described here: - \ No newline at end of file + diff --git a/docs/modders/Random_Map_Template.md b/docs/modders/Random_Map_Template.md index 309d84e8e..52f714660 100644 --- a/docs/modders/Random_Map_Template.md +++ b/docs/modders/Random_Map_Template.md @@ -2,7 +2,7 @@ ## Template format -``` javascript +```json /// Unique template name "Triangle" : { @@ -28,10 +28,11 @@ /// List of game settings that were overriden by this template. See config/gameConfig.json in vcmi install directory for possible values /// Settings defined here will always override any settings from vcmi or from mods - "settings" : { - "heroes" : { - "perPlayerOnMapCap" : 1 - } + "settings" : + { + "heroes" : + { + "perPlayerOnMapCap" : 1 } }, @@ -56,10 +57,14 @@ ## Zone format -``` javascript +```json { // Type of this zone. Possible values are: - // "playerStart", "cpuStart", "treasure", "junction" + // "playerStart" - Starting zone for a "human or CPU" players + // "cpuStart" - Starting zone for "CPU only" players + // "treasure" - Generic neutral zone + // "junction" - Neutral zone with narrow passages only. The rest of area is filled with obstacles. + // "sealed" - Decorative impassable zone completely filled with obstacles "type" : "playerStart", // relative size of zone @@ -157,4 +162,4 @@ ] } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Readme.md b/docs/modders/Readme.md index 5fba25113..783060c26 100644 --- a/docs/modders/Readme.md +++ b/docs/modders/Readme.md @@ -8,7 +8,7 @@ All content of your mod should go into **Content** directory, e.g. **Mods/myMod/ Example of how directory structure of your mod may look like: -``` +```text Mods/ myMod/ mod.json @@ -21,8 +21,8 @@ Example of how directory structure of your mod may look like: sprites/ - animation, image sets (H3 .def files or VCMI .json files) video/ - video files, .bik, .smk, .ogv .webm ``` -See [File Formats](File_Formats.md) page for more information on which formats are supported or recommended for vcmi +See [File Formats](File_Formats.md) page for more information on which formats are supported or recommended for vcmi ## Creating mod file @@ -30,7 +30,7 @@ All VCMI configuration files use [JSON format](http://en.wikipedia.org/wiki/Json Mod.json is main file in your mod and must be present in any mod. This file contains basic description of your mod, dependencies or conflicting mods (if present), list of new content and so on. Minimalistic version of this file: -``` javascript +```json { "name" : "My test mod", "description" : "My test mod that add a lot of useless stuff into the game", @@ -45,6 +45,7 @@ See [Mod file Format](Mod_File_Format.md) for its full description. ## Creation of new objects In order to create new object use following steps: + 1. Create json file with definition of new object. See list of supported object types below. 2. Add any resources needed for this object, such as images, animations or sounds. 2. Add reference to new object in corresponding section of mod.json file @@ -52,9 +53,11 @@ In order to create new object use following steps: ### List of supported new object types Random Map Generator: + - [Random Map Template](Random_Map_Template.md) Game Entities: + - [Artifact](Entities_Format/Artifact_Format.md) - [Creature Requirement](Entities_Format/Creature_Format.md) - [Creature Help](Entities_Format/Creature_Help.md) @@ -66,6 +69,7 @@ Game Entities: - [Secondary Skill](Entities_Format/Secondary_Skill_Format.md) Map objects: + - [Map Objects](Map_Object_Format.md) - - [Rewardable](Map_Objects/Rewardable.md) - - [Creature Bank](Map_Objects/Creature_Bank.md) @@ -74,6 +78,7 @@ Map objects: - - [Boat](Map_Objects/Boat.md) Other: + - [Terrain](Entities_Format/Terrain_Format.md) - [River](Entities_Format/River_Format.md) - [Road](Entities_Format/Road_Format.md) @@ -96,7 +101,8 @@ VCMI uses strings to reference objects. Examples: ### Modifying existing objects Alternatively to creating new objects, you can edit existing objects. Normally, when creating new objects you specify object name as: -``` javascript + +```json "newCreature" : { // creature parameters } @@ -104,7 +110,7 @@ Alternatively to creating new objects, you can edit existing objects. Normally, In order to access and modify existing object you need to specify mod that you wish to edit: -``` javascript +```json /// "core" specifier refers to objects that exist in H3 "core:archer" : { /// This will set health of Archer to 10 @@ -123,6 +129,7 @@ In order to access and modify existing object you need to specify mod that you w "speed" : 10 }, ``` + Note that modification of existing objects does not requires a dependency on edited mod. Such definitions will only be used by game if corresponding mod is installed and active. This allows using objects editing not just for rebalancing mods but also to provide compatibility between two different mods or to add interaction between two mods. @@ -132,6 +139,7 @@ This allows using objects editing not just for rebalancing mods but also to prov Any graphical replacer mods fall under this category. In VCMI directory **/Content** acts as mod-specific game root directory. So for example file **/Content/Data/AISHIELD.PNG** will replace file with same name from **H3Bitmap.lod** game archive. Any other files can be replaced in exactly same way. Note that replacing files from archives requires placing them into specific location: + - H3Bitmap.lod -> Data - H3Sprite.lod -> Sprites - Heroes3.snd -> Sounds @@ -145,12 +153,13 @@ This includes archives added by expansions (e.g. **H3ab_bmp.lod** uses same rule Heroes III uses custom format for storing animation: def files. These files are used to store all in-game animations as well as for some GUI elements like buttons and for icon sets. These files can be replaced by another def file but in some cases original format can't be used. This includes but not limited to: -- Replacing one (or several) icons in set -- Replacing animation with fully-colored 32-bit images + +- Replacing one (or several) icons in set +- Replacing animation with fully-colored 32-bit images In VCMI these animation files can also be replaced by json description of their content. See [Animation Format](Animation_Format.md) for full description of this format. Example: replacing single icon -``` javascript +```json { // List of replaced images "images" : @@ -191,7 +200,7 @@ Same way we can also create special stable branch for every mod under "vcmi-mods ### Getting into vcmi-mods organization Before your mod can be accepted into official mod list you need to get it into repository under "vcmi-mods" organization umbrella. To do this contact one of mod repository maintainers. If needed you can get own team within "vcmi-mods" organization. -Link to our mod will looks like that: https://github.com/vcmi-mods/adventure-ai-trace +Link to our mod will looks like that: ## Rules of repository @@ -199,8 +208,10 @@ Link to our mod will looks like that: https://github.com/vcmi-mods/adventure-ai- For sanity reasons mod identifier must only contain lower-case English characters, numbers and hyphens. - my-mod-name - 2000-new-maps +```text +my-mod-name +2000-new-maps +``` Sub-mods can be named as you like, but we strongly encourage everyone to use proper identifiers for them as well. diff --git a/docs/players/Bug_Reporting_Guidelines.md b/docs/players/Bug_Reporting_Guidelines.md index eabe6b6af..067a59e84 100644 --- a/docs/players/Bug_Reporting_Guidelines.md +++ b/docs/players/Bug_Reporting_Guidelines.md @@ -23,9 +23,9 @@ First of all, if you encounter a crash, don't re-run VCMI immediately to see if By default, log files are written to: -- Windows: Documents\My Games\vcmi\\ -- Linux: ~/.cache/vcmi/ -- Android: Android/data/is.xyz.vcmi/files/vcmi-data/cache/ +- Windows: Documents\My Games\vcmi\\ +- Linux: ~/.cache/vcmi/ +- Android: Android/data/is.xyz.vcmi/files/vcmi-data/cache/ Now you should try to reproduce encountered issue. It's best when you write how to reproduce the issue by starting a new game and taking some steps (e.g. start Arrogance map as red player and attack monster Y with hero X). If you have troubles with reproducing it this way but you can do it from a savegame - that's good too. Finally, when you are not able to reproduce the issue at all, just upload the files mentioned above. To sum up, this is a list of what's the most desired for a developer: diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index 288c9f842..57d74c8cc 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -31,6 +31,7 @@ Gives specific creature in every slot, with optional amount. Examples: `nwclotsofguns` or `vcminoldor` or `vcmimachines` - give ballista, ammo cart and first aid tent `vcmiforgeofnoldorking` or `vcmiartifacts` - give all artifacts, except spell book, spell scrolls and war machines. Artifacts added via mods included +`vcmiscrolls` - give spell scrolls for every possible spells ### Movement points @@ -70,13 +71,15 @@ Alternative usage: `vcmiexp ` - gives selected hero specified amount of `nwcbluepill` or `vcmimelkor` or `vcmilose` - player loses ### Misc + `nwctheone` or `vcmigod` - reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight ## Using cheat codes on other players + By default, all cheat codes apply to current player. Alternatively, it is possible to specify player that you want to target: - Specific players: `red`/`blue`/`green`... -- Only AI players: `ai` +- Only AI players: `ai` - All players: `all` ### Examples @@ -89,12 +92,14 @@ By default, all cheat codes apply to current player. Alternatively, it is possib ## Multiplayer chat commands Following commands can be used in multiplayer only by host player to control the session: + - `!exit` - finish the game - `!save ` - save the game into the specified file - `!kick red/blue/tan/green/orange/purple/teal/pink` - kick player of specified color from the game -- `!kick 0/1/2/3/4/5/6/7/8` - kick player of specified ID from the game (_zero indexed!_) (`0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7`) +- `!kick 0/1/2/3/4/5/6/7/8` - kick player of specified ID from the game (*zero indexed!*) (`0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7`) Following commands can be used by any player in multiplayer: + - `!help` - displays in-game list of available commands - `!cheaters` - lists players that have entered cheat at any point of the game - `!vote` - initiates voting to change one of the possible options: @@ -114,12 +119,14 @@ Windows builds of VCMI run separate console window by default, on other platform Below a list of supported commands, with their arguments wrapped in `<>` #### Game Commands + `die, fool` - quits game `save ` - saves game in given file (at the moment doesn't work) `mp` - on adventure map with a hero selected, shows heroes current movement points, max movement points on land and on water `bonuses` - shows bonuses of currently selected adventure map object #### Extract commands + `translate` - save game texts into json files `translate missing` - save untranslated game texts into json files `translate maps` - save map and campaign texts into json files @@ -131,15 +138,17 @@ Below a list of supported commands, with their arguments wrapped in `<>` `generate assets` - generate all assets at once #### AI commands + `setBattleAI ` - change battle AI used by neutral creatures to the one specified, persists through game quit `gosolo` - AI takes over until the end of turn (unlike original H3 currently causes AI to take over until typed again) `controlai <[red][blue][tan][green][orange][purple][teal][pink]>` - gives you control over specified AI player. If none is specified gives you control over all AI players `autoskip` - Toggles autoskip mode on and off. In this mode, player turns are automatically skipped and only AI moves. However, GUI is still present and allows to observe AI moves. After this option is activated, you need to end first turn manually. Press `[Shift]` before your turn starts to not skip it #### Settings + `set ` - sets special temporary settings that reset on game quit. Below some of the most notable commands: -`autoskip` - identical to `autoskip` option --`onlyAI` - run without human player, all players will be _default AI_ +-`onlyAI` - run without human player, all players will be *default AI* -`headless` - run without GUI, implies `onlyAI` is set -`showGrid` - display a square grid overlay on top of adventure map -`showBlocked` - show blocked tiles on map @@ -147,6 +156,7 @@ Below a list of supported commands, with their arguments wrapped in `<>` -`hideSystemMessages` - suppress server messages in chat #### Developer Commands + `crash` - force a game crash. It is sometimes useful to generate memory dump file in certain situations, for example game freeze `gui` - displays tree view of currently present VCMI common GUI elements `activate <0/1/2>` - activate game windows (no current use, apparently broken long ago) diff --git a/docs/players/Game_Mechanics.md b/docs/players/Game_Mechanics.md index e64f7e94e..54df67881 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -58,7 +58,7 @@ These bugs were present in original Shadow of Death game, however the team decid Some of H3 mechanics can't be straight considered as bug, but default VCMI behaviour is different: - Pathfinding. Hero can't grab artifact while flying when all tiles around it are guarded without triggering attack from guard. -- Battles. Hero that won battle, but only have temporary summoned creatures alive going to appear in tavern like if he retreated. +- Battles. Hero that won battle, but only have temporary summoned creatures alive going to appear in tavern like if he retreated. - Battles. Spells from artifacts like AOTD are autocasted on beginning of the battle, not beginning of turn. ## Adventure map features @@ -106,7 +106,7 @@ In combat, some creatures, such as Dragon or Cerberi, may attack enemies on mult - [LCtrl] + LClick – splits a single unit from the selected stack into an empty slot. - [LCtrl] + [LShift] + LClick – split single units from the selected stack into all empty hero/garrison slots - [Alt] + LClick – merge all split single units into one stack -- [Alt] + [LCtrl] + LClick - move all units of selected stack to the city's garrison or to the met hero +- [Alt] + [LCtrl] + LClick - move all units of selected stack to the city's garrison or to the met hero - [Alt] + [LShift] + LClick - dismiss selected stack` - Directly type numbers in the Split Stack window to split them in any way you wish @@ -174,6 +174,7 @@ TODO Simultaneous turns allow multiple players to act at the same time, speeding up early game phase in multiplayer games. During this phase if different players (allies or not) attempt to interact with each other, such as capture objects owned by other players (mines, dwellings, towns) or attack their heroes, game will block such actions. Interaction with same map objects at the same time, such as attacking same wandering monster is also blocked. Following options can be used to configure simultaneous turns: + - Minimal duration (at least for): this is duration during which simultaneous turns will run unconditionally. Until specified number of days have passed, simultaneous turns will never break and game will not attempt to detect contacts. - Maximal duration (at most for): this is duration after which simultaneous turns will end unconditionally, even if players still have not contacted each other. However if contact detection discovers contact between two players, simultaneous turns between them might end before specified duration. - Simultaneous turns for AI: If this option is on, AI can act at the same time as human players. Note that AI shares settings for simultaneous turns with human players - if no simultaneous turns have been set up this option has no effect. @@ -185,6 +186,7 @@ Players are considered to be "in contact" if movement range of their heroes at t Once detected, contact can never be "lost". If game detected contact between two players, this contact will remain active till the end of the game, even if their heroes move far enough from each other. Game performs contact detection once per turn, at the very start of each in-game day. Once contact detection has been performed, players that are not in contact with each other can start making turn. For example, in game with 4 players: red, blue, brown and green. If game detected contact between red and blue following will happen: + - red, brown and green will all instantly start turn - once red ends his turn, blue will be able to start his own turn (even if brown or green are still making turn) @@ -197,4 +199,4 @@ Differences compared to HD Mod version: ## Manuals and guides -- https://heroes.thelazy.net/index.php/Main_Page Wiki that aims to be a complete reference to Heroes of Might and Magic III. +- Wiki that aims to be a complete reference to Heroes of Might and Magic III. diff --git a/docs/players/Installation_Linux.md b/docs/players/Installation_Linux.md index 9440246b3..bfe324647 100644 --- a/docs/players/Installation_Linux.md +++ b/docs/players/Installation_Linux.md @@ -9,7 +9,8 @@ VCMI requires data from original Heroes 3: Shadow of Death or Complete editions. Up-to-date releases can be found in our PPA here: To install VCMI from PPA use: -``` + +```sh sudo apt-add-repository ppa:vcmi/ppa sudo apt update sudo apt install vcmi @@ -20,26 +21,31 @@ To install VCMI from PPA use: We also provide latest, unstable builds mostly suitable for testing here: In order to install from this PPA use: -``` + +```sh sudo add-apt-repository ppa:vcmi/vcmi-latest sudo apt update sudo apt install vcmi ``` + ### Ubuntu - From Ubuntu repository VCMI stable builds available in "multiverse" repository. Learn how to enable it in [Ubuntu wiki](https://help.ubuntu.com/community/Repositories/Ubuntu). Once enabled, you can install VCMI using Ubuntu Store or in terminal using following commands: -``` + +```sh sudo apt update sudo apt install vcmi ``` + Note that version available in Ubuntu is outdated. Install via PPA is preferred. ### Debian Stable VCMI version is available in "contrib" repository. Learn how to enable it in [Debian wiki](https://wiki.debian.org/SourcesList). To install VCMI from repository: -``` + +```sh sudo apt-get update sudo apt-get install vcmi ``` @@ -48,10 +54,11 @@ To install VCMI from repository: Stable VCMI version is available in RPM Fusion repository. Learn how to enable it in [wiki](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup/). To install VCMI from repository: -``` +```sh sudo dnf update sudo dnf install vcmi ``` + ### Flatpak (distribution-agnostic) Latest public release build can be installed via Flatpak. @@ -63,8 +70,8 @@ Once you have flatpak, you can install VCMI package which can be found here: -- Daily builds (unstable): -- Please report about problems on GitHub: [Bug Tracker](https://github.com/vcmi/vcmi/issues) +- Latest release (recommended): +- Daily builds (unstable): +- Please report about problems on GitHub: [Bug Tracker](https://github.com/vcmi/vcmi/issues) ## Step 2: Installing Heroes III data files **Since VCMI 1.2 you can skip this step, just run VCMI launcher and it will help you with importing H3 data. For older releases you can follow this step.** -- Install Heroes III from disk or using GOG installer. -- Place "Data", "Maps" and "Mp3" from Heroes III to: `Documents\My Games\vcmi\` +- Install Heroes III from disk or using GOG installer. +- Place "Data", "Maps" and "Mp3" from Heroes III to: `Documents\My Games\vcmi\` Create this folder if it doesnt exist yet ## Step 3: connect to the mod repository - If that's your first installation, connection to the mod repository will be configured automatically, you'll see mods available to install from VCMI launcher - -- We recommend you to install VCMI extras to support various helpful UI tweaks +- We recommend you to install VCMI extras to support various helpful UI tweaks diff --git a/docs/players/Installation_iOS.md b/docs/players/Installation_iOS.md index bf71d7225..741f2774b 100644 --- a/docs/players/Installation_iOS.md +++ b/docs/players/Installation_iOS.md @@ -6,11 +6,11 @@ You can run VCMI on iOS 12.0 and later, all devices are supported. If you wish t The easiest and recommended way to install on a non-jailbroken device is to install the [AltStore Classic](https://altstore.io/) or [Sideloadly](https://sideloadly.io/). We will use AltStore as an example below. Using this method means the VCMI certificate is auto-signed automatically. -i) Use [AltStore Windows](https://faq.altstore.io/altstore-classic/how-to-install-altstore-windows) or [AltStore macOS](https://faq.altstore.io/altstore-classic/how-to-install-altstore-macos) instructions to install the store depending on the operating system you are using. +i) Use [AltStore Windows](https://faq.altstore.io/altstore-classic/how-to-install-altstore-windows) or [AltStore macOS](https://faq.altstore.io/altstore-classic/how-to-install-altstore-macos) instructions to install the store depending on the operating system you are using. -If you're having trouble enabling "sync with this iOS device over Wi-Fi" press on the rectangular shape below "Account". Example shown below. +If you're having trouble enabling "sync with this iOS device over Wi-Fi" press on the rectangular shape below "Account". Windows example from iTunes shown below: -![image](https://github.com/user-attachments/assets/74fe2ca2-b55c-4b05-b083-89df604248f3) +![iTunes](images/itunes.jpg) ii) Download the VCMI-iOS.ipa file on your iOS device directly from the [latest releases](https://github.com/vcmi/vcmi/releases/latest). @@ -19,15 +19,14 @@ iii) To install the .ipa file on your device do one of the following: - In AltStore go to >My Apps > press + in the top left corner. Select VCMI-iOS.ipa to install, - or drag and drop the .ipa file into your iOS device in iTunes - ## Step 2: Installing Heroes III data files -If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_magic_3_complete_edition), you can download the files directly from the browser in the device. - -Launch VCMI app on the device and the launcher will prompt two files to complete the installation. Select the **.bin** file first, then the **.exe** file. This may take a few seconds. Please be patient. +If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_magic_3_complete_edition), you can download the files directly from the browser in the device. +Launch VCMI app on the device and the launcher will prompt two files to complete the installation. Select the **.bin** file first, then the **.exe** file. This may take a few seconds. Please be patient. ## Step 3: Configuration settings + Once you have installed VCMI and have the launcher opened, select Settings on the left bar. The following Video settings are recommended: - Lower reserved screen area to zero. @@ -42,7 +41,7 @@ Together, the two options should eliminate black bars and enable full screen VCM To run on a non-jailbroken device you need to sign the IPA file, you have the following aternative options: -- if you're on iOS 14.0-15.4.1, you can try . +- if you're on iOS 14.0-15.4.1, you can try . - Get signer tool [here](https://dantheman827.github.io/ios-app-signer/) and a guide [here](https://forum.kodi.tv/showthread.php?tid=245978) (it's for Kodi, but the logic is the same). Signing with this app can only be done on macOS. - [Create signing assets on macOS from terminal](https://github.com/kambala-decapitator/xcode-auto-signing-assets). In the command replace `your.bundle.id` with something like `com.MY-NAME.vcmi`. After that use the above signer tool. - [Sign from any OS (Rust)](https://github.com/indygreg/PyOxidizer/tree/main/tugger-code-signing) / [alternative project (C++)](https://github.com/zhlynn/zsign). You'd still need to find a way to create signing assets (private key and provisioning profile) though. @@ -50,12 +49,13 @@ To run on a non-jailbroken device you need to sign the IPA file, you have the fo The easiest way to install the ipa on your device is to do one of the following: - In AltStore go to >My Apps > press + in the top left corner. Select VCMI-iOS.ipa to install or - - Drag and drop the .ipa file into your iOS device in iTunes Alternatively, to install the signed ipa on your device, you can use Xcode or Apple Configurator (available on the Mac App Store for free). The latter also allows installing ipa from the command line, here's an example that assumes you have only 1 device connected to your Mac and the signed ipa is on your desktop: - /Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa +```sh +/Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa +``` ## Alternative Step 2: Installing Heroes III data files @@ -80,7 +80,7 @@ You can also upload files with Xcode. You need to prepare "container" for that. 3. Open Devices and Simulators window: Cmd+Shift+2 or Menu - Window - Devices and Simulators 4. Select your device 5. Select VCMI -6. In the bottom find "three dots" or "cogwheel" button (it should be next to + - buttons) - click it - select Download Container... +6. In the bottom find "three dots" or "cogwheel" button (it should be next to + - buttons) - click it - select Download Container... 7. Place the game directories inside the downloaded container - AppData - Documents 8. Click the "three dots" / "cogwheel" button in Xcode again - Replace Container... - select the downloaded container 9. Wait until Xcode finishes copying, progress is visible (although it might be "indefinite") diff --git a/docs/players/Installation_macOS.md b/docs/players/Installation_macOS.md index 23c3def95..d327dc62e 100644 --- a/docs/players/Installation_macOS.md +++ b/docs/players/Installation_macOS.md @@ -5,7 +5,7 @@ - The latest release (recommended): - manually: - via Homebrew: `brew install --cask --no-quarantine vcmi/vcmi/vcmi` -- Daily builds (might be unstable) +- Daily builds (might be unstable) - Intel (x86_64) builds: - Apple Silicon (arm64) builds: @@ -21,5 +21,5 @@ If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_mag ### Step 2.b: Installing by the classic way -1. Find a way to unpack Windows Heroes III or GOG installer. For example, use `vcmibuilder` script inside app bundle or install the game with [CrossOver](https://www.codeweavers.com/crossover) or [Kegworks](https://github.com/Kegworks-App/Kegworks). -2. Place or symlink **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` +1. Find a way to unpack Windows Heroes III or GOG installer. For example, use `vcmibuilder` script inside app bundle or install the game with [CrossOver](https://www.codeweavers.com/crossover) or [Kegworks](https://github.com/Kegworks-App/Kegworks). +2. Place or symlink **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` diff --git a/docs/players/Privacy_Policy.md b/docs/players/Privacy_Policy.md index 5bd841505..b35074107 100644 --- a/docs/players/Privacy_Policy.md +++ b/docs/players/Privacy_Policy.md @@ -13,4 +13,4 @@ VCMI team does not collect any data produced by VCMI app. All game files, logs, ## Multiplayer -If you decide to play with other users via Internet there are two roles. The host is the one who provides the game server. The clients are the other players who connect to the host. The host provides to the client its IP address in order to establish connections. The clients and the host during the gameplay exchange their usernames, messages and other game activity. All this data is collected and stored by the host. VCMI team does not collect and store any multiplayer data. \ No newline at end of file +If you decide to play with other users via Internet there are two roles. The host is the one who provides the game server. The clients are the other players who connect to the host. The host provides to the client its IP address in order to establish connections. The clients and the host during the gameplay exchange their usernames, messages and other game activity. All this data is collected and stored by the host. VCMI team does not collect and store any multiplayer data. diff --git a/docs/players/images/itunes.jpg b/docs/players/images/itunes.jpg new file mode 100644 index 000000000..1301fbf6d Binary files /dev/null and b/docs/players/images/itunes.jpg differ diff --git a/docs/translators/Translations.md b/docs/translators/Translations.md index c226a620a..c13c2b8ca 100644 --- a/docs/translators/Translations.md +++ b/docs/translators/Translations.md @@ -23,6 +23,7 @@ This is list of all languages that are currently supported by VCMI. If your lang - Vietnamese ## Progress of the translations + You can see the current progress of the different translations here: [Translation progress](https://github.com/vcmi/vcmi-translation-status) @@ -32,17 +33,18 @@ The page will be automatically updated once a week. VCMI allows translating game data into languages other than English. In order to translate Heroes III in your language easiest approach is to: -- Copy existing translation, such as English translation from here: https://github.com/vcmi-mods/h3-for-vcmi-englisation (delete sound and video folders) -- Copy text-free images from here: https://github.com/vcmi-mods/empty-translation +- Copy existing translation, such as English translation from here: (delete sound and video folders) +- Copy text-free images from here: - Rename mod to indicate your language, preferred form is "(language)-translation" - Update mod.json to match your mod - Translate all texts strings from `game.json`, `campaigns.json` and `maps.json` - Replace images in data and sprites with translated ones (or delete it if you don't want to translate them) -- If unicode characters needed for language: Create a submod with a free font like here: https://github.com/vcmi-mods/vietnamese-translation/tree/vcmi-1.4/vietnamese-translation/mods/VietnameseTrueTypeFonts +- If unicode characters needed for language: Create a submod with a free font like here: If you can't produce some content on your own (like the images or the sounds): + - Create a `README.md` file at the root of the mod -- Write into the file the translations and the detailled location +- Write into the file the translations and the **detailled** location This way, a contributor that is not a native speaker can do it for you in the future. @@ -57,8 +59,10 @@ This will export all strings from game into `Documents/My Games/VCMI/extracted/t To export maps and campaigns, use '/translate maps' command instead. ### Video subtitles + It's possible to add video subtitles. Create a JSON file in `video` folder of translation mod with the name of the video (e.g. `H3Intro.json`): -``` + +```json [ { "timeStart" : 5.640, // start time, seconds @@ -84,6 +88,7 @@ Before you start, make sure that you have copy of VCMI source code. If you are n ### Translation of in-game data In order to translate in-game data you need: + - Add section with your language to `/Mods/VCMI/mod.json`, similar to other languages - Copy English translation file in `/Mods/VCMI/config/vcmi/english.json` and rename it to name of your language. Note that while you can copy any language other than English, other files might not be up to date and may have missing strings. - Translate copied file to your language. @@ -94,7 +99,7 @@ After this, you can set language in Launcher to your language and start game. Al VCMI Launcher and Map Editor use translation system provided by Qt framework so it requires slightly different approach than in-game translations: -- Install Qt Linguist. You can find find standalone version here: https://download.qt.io/linguist_releases/ +- Install Qt Linguist. You can find find standalone version here: - Open `/launcher/translation/` directory, copy `english.ts` file and rename it to your language - Open `/launcher/CMakeLists.txt` file with a text editor. In there you need to find list of existing translation files and add new file to the list. - Launch Qt Linguist, select Open and navigate to your copied file @@ -112,23 +117,27 @@ TODO: how to test translation locally The [AppStream](https://freedesktop.org/software/appstream/docs/chap-Metadata.html) [metainfo file](https://github.com/vcmi/vcmi/blob/develop/launcher/eu.vcmi.VCMI.metainfo.xml) is used for Linux software centers. It can be translated using a text editor or using [jdAppStreamEdit](https://flathub.org/apps/page.codeberg.JakobDev.jdAppStreamEdit): + - Install jdAppStreamEdit - Open `/launcher/eu.vcmi.VCMI.metainfo.xml` - Translate and save the file ##### Desktop file + - Edit `/launcher/vcmilauncher.desktop` and `/launcher/vcmieditor.desktop` - Add `GenericName[xyz]` and `Comment[xyz]` with your language code and translation ##### Translation of Android Launcher + - Copy `/android/vcmi-app/src/main/res/values/strings.xml` to `/android/vcmi-app/src/main/res/values-xyz/strings.xml` (`xyz` is your language code) - Translate this file -See also here: https://developer.android.com/guide/topics/resources/localization +See also here: ### Submitting changes Once you have finished with translation you need to submit these changes to vcmi team using git or Github Desktop + - Commit all your changed files - Push changes to your forked repository - Create pull request in VCMI repository with your changes @@ -152,8 +161,10 @@ If your mod also contains maps or campaigns that you want to translate, then use If you want to update existing translation, you can use '/translate missing' command that will export only strings that were not translated ### Translating mod information + In order to display information in Launcher in language selected by user add following block into your `mod.json`: -``` + +```json "" : { "name" : "", "description" : "", @@ -163,6 +174,7 @@ In order to display information in Launcher in language selected by user add fol ] }, ``` + However, normally you don't need to use block for English. Instead, English text should remain in root section of your `mod.json` file, to be used when game can not find translated version. ### Translating in-game strings @@ -174,7 +186,9 @@ Use any text editor (Notepad++ is recommended for Windows) and translate all str ## Developers documentation ### Adding new languages + In order to add new language it needs to be added in multiple locations in source code: + - Generate new .ts files for launcher and map editor, either by running `lupdate` with name of new .ts or by copying `english.ts` and editing language tag in the header. - Add new language into `lib/Languages.h` entry. This will trigger static_assert's in places that needs an update in code - Add new language into json schemas validation list - settings schema and mod schema @@ -187,7 +201,8 @@ Also, make full search for a name of an existing language to ensure that there a At the moment, build system will generate binary translation files (`.qs`) that can be opened by Qt. However, any new or changed lines will not be added into existing .ts files. In order to update `.ts` files manually, open command line shell in `mapeditor` or `launcher` source directories and execute command -``` + +```sh lupdate -no-obsolete * -ts translation/*.ts ``` @@ -197,5 +212,6 @@ There *may* be a way to do the same via QtCreator UI or via CMake, if you find o ### Updating translation of Launcher and Map Editor using new .ts file from translators Generally, this should be as simple as overwriting old files. Things that may be necessary if translation update is not visible in executable: + - Rebuild subproject (map editor/launcher). - Regenerate translations via `lupdate -no-obsolete * -ts translation/*.ts` diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a56893838..1dad5bc7f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -7,10 +7,11 @@ set(launcher_SRCS StdInc.cpp aboutProject/aboutproject_moc.cpp modManager/cdownloadmanager_moc.cpp - modManager/cmodlist.cpp - modManager/cmodlistmodel_moc.cpp + modManager/modstateitemmodel_moc.cpp modManager/cmodlistview_moc.cpp - modManager/cmodmanager.cpp + modManager/modstatecontroller.cpp + modManager/modstatemodel.cpp + modManager/modstate.cpp modManager/imageviewer_moc.cpp modManager/chroniclesextractor.cpp settingsView/csettingsview_moc.cpp @@ -37,10 +38,11 @@ set(launcher_HEADERS StdInc.h aboutProject/aboutproject_moc.h modManager/cdownloadmanager_moc.h - modManager/cmodlist.h - modManager/cmodlistmodel_moc.h + modManager/modstateitemmodel_moc.h modManager/cmodlistview_moc.h - modManager/cmodmanager.h + modManager/modstatecontroller.h + modManager/modstatemodel.h + modManager/modstate.h modManager/imageviewer_moc.h modManager/chroniclesextractor.h settingsView/csettingsview_moc.h diff --git a/launcher/firstLaunch/firstlaunch_moc.cpp b/launcher/firstLaunch/firstlaunch_moc.cpp index cd0daeadd..194a45c45 100644 --- a/launcher/firstLaunch/firstlaunch_moc.cpp +++ b/launcher/firstLaunch/firstlaunch_moc.cpp @@ -359,8 +359,9 @@ void FirstLaunchView::extractGogData() return; // should not happen - but avoid deleting wrong folder in any case QString tmpFileExe = tempDir.filePath("h3_gog.exe"); + QString tmpFileBin = tempDir.filePath("h3_gog-1.bin"); QFile(fileExe).copy(tmpFileExe); - QFile(fileBin).copy(tempDir.filePath("h3_gog-1.bin")); + QFile(fileBin).copy(tmpFileBin); QString errorText{}; @@ -387,6 +388,10 @@ void FirstLaunchView::extractGogData() ui->progressBarGog->setValue(progress * 100); qApp->processEvents(); }); + + QString hashError; + if(!errorText.isEmpty()) + hashError = Innoextract::getHashError(tmpFileExe, tmpFileBin, fileExe, fileBin); ui->progressBarGog->setVisible(false); ui->pushButtonGogInstall->setVisible(true); @@ -396,7 +401,11 @@ void FirstLaunchView::extractGogData() if(!errorText.isEmpty() || dirData.empty() || QDir(tempDir.filePath(dirData.front())).entryList({"*.lod"}, QDir::Filter::Files).empty()) { if(!errorText.isEmpty()) + { QMessageBox::critical(this, tr("Extracting error!"), errorText, QMessageBox::Ok, QMessageBox::Ok); + if(!hashError.isEmpty()) + QMessageBox::critical(this, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok); + } else QMessageBox::critical(this, tr("No Heroes III data!"), tr("Selected files do not contain Heroes III data!"), QMessageBox::Ok, QMessageBox::Ok); tempDir.removeRecursively(); diff --git a/launcher/icons/submod-disabled.png b/launcher/icons/submod-disabled.png new file mode 100644 index 000000000..7e7c1a142 Binary files /dev/null and b/launcher/icons/submod-disabled.png differ diff --git a/launcher/icons/submod-enabled.png b/launcher/icons/submod-enabled.png new file mode 100644 index 000000000..3eee77480 Binary files /dev/null and b/launcher/icons/submod-enabled.png differ diff --git a/launcher/innoextract.cpp b/launcher/innoextract.cpp index e4c40c00a..20e1e7182 100644 --- a/launcher/innoextract.cpp +++ b/launcher/innoextract.cpp @@ -60,3 +60,105 @@ QString Innoextract::extract(QString installer, QString outDir, std::function knownHashes = { + { H3_COMPLETE, "english", 822520, 1005040617, "66646a353b06417fa12c6384405688c84a315cc1", "c624e2071f4e35386765ab044ad5860ac245b7f4" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(28740).exe + { H3_COMPLETE, "french", 824960, 997305870, "072f1d4466ff16444d8c7949c6530448a9c53cfa", "9b6b451d2bd2f8b4be159e62fa6d32e87ee10455" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(french)_(28740).exe + { H3_COMPLETE, "polish", 822288, 849286313, "74ffde00156dd5a8e237668f87213387f0dd9c7c", "2523cf9943043ae100186f89e4ebf7c28be09804" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(polish)_(28740).exe + { H3_COMPLETE, "russian", 821608, 980398466, "88ccae41e66da58ba4ad62024d97dfe69084f825", "58f1b3c813a1953992bba1f9855c47d01c897db8" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(russian)_(28740).exe + { H3_COMPLETE, "english", 820288, 1006275333, "ca68adb8c2d8c6b3afa17a595ad70c2cec062b5a", "2715e10e91919d05377d39fd879d43f9f0cb9f87" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(3.2)_gog_0.1_(77075).exe + { H3_COMPLETE, "french", 822688, 998540653, "fbb300eeef52f5d81a571a178723b19313e3856d", "4f4d90ff2f60968616766237664744bc54754500" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(3.2)_gog_0.1_(french)_(77075).exe + { H3_COMPLETE, "polish", 819904, 851750601, "a413b0b9f3d5ca3e1a57e84a42de28c67d77b1a7", "fd9fe58bcbb8b442e8cfc299d90f1d503f281d40" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(3.2)_gog_0.1_(polish)_(77075).exe + { H3_COMPLETE, "russian", 819416, 981633128, "e84eedf62fe2e5f9171a7e1ce6e99315a09ce41f", "49cc683395c0cf80830bfa66e42bb5dfdb7aa124" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(3.2)_gog_0.1_(russian)_(77075).exe + { CHR, "english", 485694752, 0, "44e4fc2c38261a1c2a57d5198f44493210e8fc1a", "" }, // setup_heroes_chronicles_chapter1_2.1.0.42.exe + { CHR, "english", 493102840, 0, "b479a3272cf4b57a6b7fc499df5eafb624dcd6de", "" }, // setup_heroes_chronicles_chapter2_2.1.0.43.exe + { CHR, "english", 470364128, 0, "5ad36d822e1700c9ecf93b78652900a52518146b", "" }, // setup_heroes_chronicles_chapter3_2.1.0.41.exe + { CHR, "english", 469211296, 0, "5deb374a2e188ed14e8f74ad1284c45e46adf760", "" }, // setup_heroes_chronicles_chapter4_2.1.0.42.exe + { CHR, "english", 447497560, 0, "a6daa6ed56c840f3be7ad6ad920a2f9f2439acc8", "" }, // setup_heroes_chronicles_chapter5_2.1.0.42.exe + { CHR, "english", 447430456, 0, "93a42dd24453f36e7020afc61bca05b8461a3f04", "" }, // setup_heroes_chronicles_chapter6_2.1.0.42.exe + { CHR, "english", 481583720, 0, "d74b042015f3c5b667821c5d721ac3d2fdbf43fc", "" }, // setup_heroes_chronicles_chapter7_2.1.0.42.exe + { CHR, "english", 462976008, 0, "9039050e88b9dabcdb3ffa74b33e6aa86a20b7d9", "" }, // setup_heroes_chronicles_chapter8_2.1.0.42.exe + }; + + auto doHash = [](QFile f){ + fileinfo tmp; + + if(f.open(QFile::ReadOnly)) { + QCryptographicHash hash(QCryptographicHash::Algorithm::Sha1); + if(hash.addData(&f)) + tmp.hash = hash.result().toHex().toLower().toStdString(); + tmp.size = f.size(); + } + + return tmp; + }; + + fileinfo exeInfo; + fileinfo binInfo; + fileinfo exeInfoOriginal; + fileinfo binInfoOriginal; + + exeInfo = doHash(QFile(exeFile)); + if(!binFile.isEmpty()) + binInfo = doHash(QFile(binFile)); + exeInfoOriginal = doHash(QFile(exeFileOriginal)); + if(!binFileOriginal.isEmpty()) + binInfoOriginal = doHash(QFile(binFileOriginal)); + + if(exeInfo.hash.empty() || (!binFile.isEmpty() && binInfo.hash.empty())) + return QString{}; // hashing not possible -> previous error is enough + + QString hashOutput = tr("SHA1 hash of provided files:\nExe (%1 bytes):\n%2").arg(QString::number(exeInfo.size), QString::fromStdString(exeInfo.hash)); + if(!binInfo.hash.empty()) + hashOutput += tr("\nBin (%1 bytes):\n%2").arg(QString::number(binInfo.size), QString::fromStdString(binInfo.hash)); + + if((!exeInfoOriginal.hash.empty() && exeInfo.hash != exeInfoOriginal.hash) || (!binInfoOriginal.hash.empty() && !binFile.isEmpty() && !binFileOriginal.isEmpty() && binInfo.hash != binInfoOriginal.hash)) + return tr("Internal copy process failed. Enough space on device?\n\n%1").arg(hashOutput); + + QString foundKnown; + QString exeLang; + QString binLang; + auto find = [exeInfo, binInfo](const data & d) { return (!d.exe.empty() && d.exe == exeInfo.hash) || (!d.bin.empty() && d.bin == binInfo.hash);}; + auto it = std::find_if(knownHashes.begin(), knownHashes.end(), find); + while(it != knownHashes.end()){ + auto lang = QString::fromStdString((*it).language); + foundKnown += "\n" + (exeInfo.hash == (*it).exe ? tr("Exe") : tr("Bin")) + " - " + lang; + if(exeInfo.hash == (*it).exe) + exeLang = lang; + else + binLang = lang; + it = std::find_if(++it, knownHashes.end(), find); + } + + if(!exeLang.isEmpty() && !binLang.isEmpty() && exeLang != binLang && !binFile.isEmpty()) + return tr("Language mismatch!\n%1\n\n%2").arg(foundKnown, hashOutput); + else if((!exeLang.isEmpty() || !binLang.isEmpty()) && !binFile.isEmpty()) + return tr("Only one file known! Maybe files are corrupted? Please download again.\n%1\n\n%2").arg(foundKnown, hashOutput); + else if(!exeLang.isEmpty() && binFile.isEmpty()) + return QString{}; + else if(!exeLang.isEmpty() && !binFile.isEmpty() && exeLang == binLang) + return QString{}; + + return tr("Unknown files! Maybe files are corrupted? Please download again.\n\n%1").arg(hashOutput); +} diff --git a/launcher/innoextract.h b/launcher/innoextract.h index 4e5ee0a10..e61871bff 100644 --- a/launcher/innoextract.h +++ b/launcher/innoextract.h @@ -13,4 +13,5 @@ class Innoextract : public QObject { public: static QString extract(QString installer, QString outDir, std::function cb = nullptr); + static QString getHashError(QString exeFile, QString binFile, QString exeFileOriginal, QString binFileOriginal); }; diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index 488d6d73f..6b0b245e5 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -206,11 +206,6 @@ void MainWindow::on_startEditorButton_clicked() startEditor({}); } -const CModList & MainWindow::getModList() const -{ - return ui->modlistView->getModList(); -} - CModListView * MainWindow::getModView() { return ui->modlistView; diff --git a/launcher/mainwindow_moc.h b/launcher/mainwindow_moc.h index 6581086ff..9c0dc8d7d 100644 --- a/launcher/mainwindow_moc.h +++ b/launcher/mainwindow_moc.h @@ -46,7 +46,6 @@ public: explicit MainWindow(QWidget * parent = nullptr); ~MainWindow() override; - const CModList & getModList() const; CModListView * getModView(); void updateTranslation(); diff --git a/launcher/modManager/chroniclesextractor.cpp b/launcher/modManager/chroniclesextractor.cpp index 6f58bdd06..cbac28845 100644 --- a/launcher/modManager/chroniclesextractor.cpp +++ b/launcher/modManager/chroniclesextractor.cpp @@ -84,7 +84,10 @@ bool ChroniclesExtractor::extractGogInstaller(QString file) if(!errorText.isEmpty()) { + QString hashError = Innoextract::getHashError(file, {}, {}, {}); QMessageBox::critical(parent, tr("Extracting error!"), errorText); + if(!hashError.isEmpty()) + QMessageBox::critical(parent, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok); return false; } diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp deleted file mode 100644 index fad621601..000000000 --- a/launcher/modManager/cmodlist.cpp +++ /dev/null @@ -1,434 +0,0 @@ -/* - * cmodlist.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "cmodlist.h" - -#include "../lib/CConfigHandler.h" -#include "../../lib/filesystem/CFileInputStream.h" -#include "../../lib/GameConstants.h" -#include "../../lib/modding/CModVersion.h" - -QString CModEntry::sizeToString(double size) -{ - static const std::array sizes { - QT_TRANSLATE_NOOP("File size", "%1 B"), - QT_TRANSLATE_NOOP("File size", "%1 KiB"), - QT_TRANSLATE_NOOP("File size", "%1 MiB"), - QT_TRANSLATE_NOOP("File size", "%1 GiB"), - QT_TRANSLATE_NOOP("File size", "%1 TiB") - }; - size_t index = 0; - while(size > 1024 && index < sizes.size()) - { - size /= 1024; - index++; - } - return QCoreApplication::translate("File size", sizes[index]).arg(QString::number(size, 'f', 1)); -} - -CModEntry::CModEntry(QVariantMap repository, QVariantMap localData, QVariantMap modSettings, QString modname) - : repository(repository), localData(localData), modSettings(modSettings), modname(modname) -{ -} - -bool CModEntry::isEnabled() const -{ - if(!isInstalled()) - return false; - - if (!isVisible()) - return false; - - return modSettings["active"].toBool(); -} - -bool CModEntry::isDisabled() const -{ - if(!isInstalled()) - return false; - return !isEnabled(); -} - -bool CModEntry::isAvailable() const -{ - if(isInstalled()) - return false; - return !repository.isEmpty(); -} - -bool CModEntry::isUpdateable() const -{ - if(!isInstalled()) - return false; - - auto installedVer = localData["installedVersion"].toString().toStdString(); - auto availableVer = repository["latestVersion"].toString().toStdString(); - - return (CModVersion::fromString(installedVer) < CModVersion::fromString(availableVer)); -} - -bool isCompatible(const QVariantMap & compatibility) -{ - auto compatibleMin = CModVersion::fromString(compatibility["min"].toString().toStdString()); - auto compatibleMax = CModVersion::fromString(compatibility["max"].toString().toStdString()); - - return (compatibleMin.isNull() || CModVersion::GameVersion().compatible(compatibleMin, true, true)) - && (compatibleMax.isNull() || compatibleMax.compatible(CModVersion::GameVersion(), true, true)); -} - -bool CModEntry::isCompatible() const -{ - return ::isCompatible(localData["compatibility"].toMap()); -} - -bool CModEntry::isEssential() const -{ - return getName() == "vcmi"; -} - -bool CModEntry::isInstalled() const -{ - return !localData.isEmpty(); -} - -bool CModEntry::isVisible() const -{ - if (isCompatibilityPatch()) - { - if (isSubmod()) - return false; - } - - if (isTranslation()) - { - // Do not show not installed translation mods to languages other than player language - if (localData.empty() && getBaseValue("language") != QString::fromStdString(settings["general"]["language"].String()) ) - return false; - } - - return !localData.isEmpty() || (!repository.isEmpty() && !repository.contains("mod")); -} - -bool CModEntry::isTranslation() const -{ - return getBaseValue("modType").toString() == "Translation"; -} - -bool CModEntry::isCompatibilityPatch() const -{ - return getBaseValue("modType").toString() == "Compatibility"; -} - -bool CModEntry::isSubmod() const -{ - return getName().contains('.'); -} - -int CModEntry::getModStatus() const -{ - int status = 0; - if(isEnabled()) - status |= ModStatus::ENABLED; - if(isInstalled()) - status |= ModStatus::INSTALLED; - if(isUpdateable()) - status |= ModStatus::UPDATEABLE; - - return status; -} - -QString CModEntry::getName() const -{ - return modname; -} - -QVariant CModEntry::getValue(QString value) const -{ - return getValueImpl(value, true); -} - -QStringList CModEntry::getDependencies() const -{ - QStringList result; - for (auto const & entry : getValue("depends").toStringList()) - result.push_back(entry.toLower()); - return result; -} - -QStringList CModEntry::getConflicts() const -{ - QStringList result; - for (auto const & entry : getValue("conflicts").toStringList()) - result.push_back(entry.toLower()); - return result; -} - -QVariant CModEntry::getBaseValue(QString value) const -{ - return getValueImpl(value, false); -} - -QVariant CModEntry::getValueImpl(QString value, bool localized) const - -{ - QString langValue = QString::fromStdString(settings["general"]["language"].String()); - - // Priorities - // 1) data from newest version - // 2) data from preferred language - - bool useRepositoryData = repository.contains(value); - - if(repository.contains(value) && localData.contains(value)) - { - // value is present in both repo and locally installed. Select one from latest version - auto installedVer = localData["installedVersion"].toString().toStdString(); - auto availableVer = repository["latestVersion"].toString().toStdString(); - - useRepositoryData = CModVersion::fromString(installedVer) < CModVersion::fromString(availableVer); - } - - auto & storage = useRepositoryData ? repository : localData; - - if(localized && storage.contains(langValue)) - { - auto langStorage = storage[langValue].toMap(); - if (langStorage.contains(value)) - return langStorage[value]; - } - - if(storage.contains(value)) - return storage[value]; - - return QVariant(); -} - -QVariantMap CModList::copyField(QVariantMap data, QString from, QString to) const -{ - QVariantMap renamed; - - for(auto it = data.begin(); it != data.end(); it++) - { - QVariantMap modConf = it.value().toMap(); - - modConf.insert(to, modConf.value(from)); - renamed.insert(it.key(), modConf); - } - return renamed; -} - -void CModList::reloadRepositories() -{ - cachedMods.clear(); -} - -void CModList::resetRepositories() -{ - repositories.clear(); - cachedMods.clear(); -} - -void CModList::addRepository(QVariantMap data) -{ - for(auto & key : data.keys()) - data[key.toLower()] = data.take(key); - repositories.push_back(copyField(data, "version", "latestVersion")); - - cachedMods.clear(); -} - -void CModList::setLocalModList(QVariantMap data) -{ - localModList = copyField(data, "version", "installedVersion"); - cachedMods.clear(); -} - -void CModList::setModSettings(QVariant data) -{ - modSettings = data.toMap(); - cachedMods.clear(); -} - -void CModList::modChanged(QString modID) -{ - cachedMods.clear(); -} - -static QVariant getValue(QVariant input, QString path) -{ - if(path.size() > 1) - { - QString entryName = path.section('/', 0, 1); - QString remainder = "/" + path.section('/', 2, -1); - - entryName.remove(0, 1); - return getValue(input.toMap().value(entryName), remainder); - } - else - { - return input; - } -} - -const CModEntry & CModList::getMod(QString modName) const -{ - modName = modName.toLower(); - - auto it = cachedMods.find(modName); - - if (it != cachedMods.end()) - return it.value(); - - auto itNew = cachedMods.insert(modName, getModUncached(modName)); - return *itNew; -} - -CModEntry CModList::getModUncached(QString modname) const -{ - QVariantMap repo; - QVariantMap local = localModList[modname].toMap(); - QVariantMap settings; - - QString path = modname; - path = "/" + path.replace(".", "/mods/"); - QVariant conf = getValue(modSettings, path); - - if(conf.isNull()) - { - settings["active"] = !local.value("keepDisabled").toBool(); - } - else - { - if(!conf.toMap().isEmpty()) - { - settings = conf.toMap(); - if(settings.value("active").isNull()) - settings["active"] = !local.value("keepDisabled").toBool(); - } - else - settings.insert("active", conf); - } - - if(settings["active"].toBool()) - { - QString rootPath = path.section('/', 0, 1); - if(path != rootPath) - { - conf = getValue(modSettings, rootPath); - const auto confMap = conf.toMap(); - if(!conf.isNull() && !confMap["active"].isNull() && !confMap["active"].toBool()) - { - settings = confMap; - } - } - } - - if(settings.value("active").toBool()) - { - if(!::isCompatible(local.value("compatibility").toMap())) - settings["active"] = false; - } - - for(auto entry : repositories) - { - QVariant repoVal = getValue(entry, path); - if(repoVal.isValid()) - { - auto repoValMap = repoVal.toMap(); - if(::isCompatible(repoValMap["compatibility"].toMap())) - { - if(repo.empty() - || CModVersion::fromString(repo["version"].toString().toStdString()) - < CModVersion::fromString(repoValMap["version"].toString().toStdString())) - { - //take valid download link, screenshots and mod size before assignment - auto download = repo.value("download"); - auto screenshots = repo.value("screenshots"); - auto size = repo.value("downloadSize"); - repo = repoValMap; - if(repo.value("download").isNull()) - { - repo["download"] = download; - if(repo.value("screenshots").isNull()) //taking screenshot from the downloadable version - repo["screenshots"] = screenshots; - } - if(repo.value("downloadSize").isNull()) - repo["downloadSize"] = size; - } - } - } - } - - return CModEntry(repo, local, settings, modname); -} - -bool CModList::hasMod(QString modname) const -{ - if(localModList.contains(modname)) - return true; - - for(auto entry : repositories) - if(entry.contains(modname)) - return true; - - return false; -} - -QStringList CModList::getRequirements(QString modname) -{ - QStringList ret; - - if(hasMod(modname)) - { - auto mod = getMod(modname); - - for(auto entry : mod.getDependencies()) - ret += getRequirements(entry.toLower()); - } - ret += modname; - - return ret; -} - -QVector CModList::getModList() const -{ - QSet knownMods; - QVector modList; - for(auto repo : repositories) - { - for(auto it = repo.begin(); it != repo.end(); it++) - { - knownMods.insert(it.key().toLower()); - } - } - for(auto it = localModList.begin(); it != localModList.end(); it++) - { - knownMods.insert(it.key().toLower()); - } - - for(auto entry : knownMods) - { - modList.push_back(entry); - } - return modList; -} - -QVector CModList::getChildren(QString parent) const -{ - QVector children; - - int depth = parent.count('.') + 1; - for(const QString & mod : getModList()) - { - if(mod.count('.') == depth && mod.startsWith(parent)) - children.push_back(mod); - } - return children; -} diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h deleted file mode 100644 index a9c595a08..000000000 --- a/launcher/modManager/cmodlist.h +++ /dev/null @@ -1,112 +0,0 @@ -/* - * cmodlist.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include -#include -#include - -namespace ModStatus -{ -enum EModStatus -{ - MASK_NONE = 0, - ENABLED = 1, - INSTALLED = 2, - UPDATEABLE = 4, - MASK_ALL = 255 -}; -} - -class CModEntry -{ - // repository contains newest version only (if multiple are available) - QVariantMap repository; - QVariantMap localData; - QVariantMap modSettings; - - QString modname; - - QVariant getValueImpl(QString value, bool localized) const; -public: - CModEntry(QVariantMap repository, QVariantMap localData, QVariantMap modSettings, QString modname); - - // installed and enabled - bool isEnabled() const; - // installed but disabled - bool isDisabled() const; - // available in any of repositories but not installed - bool isAvailable() const; - // installed and greater version exists in repository - bool isUpdateable() const; - // installed - bool isInstalled() const; - // vcmi essential files - bool isEssential() const; - // checks if version is compatible with vcmi - bool isCompatible() const; - // returns true if mod should be visible in Launcher - bool isVisible() const; - // returns true if mod type is Translation - bool isTranslation() const; - // returns true if mod type is Compatibility - bool isCompatibilityPatch() const; - // returns true if this is a submod - bool isSubmod() const; - - // see ModStatus enum - int getModStatus() const; - - QString getName() const; - - // get value of some field in mod structure. Returns empty optional if value is not present - QVariant getValue(QString value) const; - QVariant getBaseValue(QString value) const; - - QStringList getDependencies() const; - QStringList getConflicts() const; - - static QString sizeToString(double size); -}; - -class CModList -{ - QVector repositories; - QVariantMap localModList; - QVariantMap modSettings; - - mutable QMap cachedMods; - - QVariantMap copyField(QVariantMap data, QString from, QString to) const; - - CModEntry getModUncached(QString modname) const; -public: - virtual void resetRepositories(); - virtual void reloadRepositories(); - virtual void addRepository(QVariantMap data); - virtual void setLocalModList(QVariantMap data); - virtual void setModSettings(QVariant data); - virtual void modChanged(QString modID); - - // returns mod by name. Note: mod MUST exist - const CModEntry & getMod(QString modname) const; - - // returns list of all mods necessary to run selected one, including mod itself - // order is: first mods in list don't have any dependencies, last mod is modname - // note: may include mods not present in list - QStringList getRequirements(QString modname); - - bool hasMod(QString modname) const; - - // returns list of all available mods - QVector getModList() const; - - QVector getChildren(QString parent) const; -}; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index a0b11b6a3..64bc7b99f 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -17,8 +17,9 @@ #include #include -#include "cmodlistmodel_moc.h" -#include "cmodmanager.h" +#include "modstatemodel.h" +#include "modstateitemmodel_moc.h" +#include "modstatecontroller.h" #include "cdownloadmanager_moc.h" #include "chroniclesextractor.h" #include "../settingsView/csettingsview_moc.h" @@ -31,18 +32,21 @@ #include "../../lib/texts/Languages.h" #include "../../lib/modding/CModVersion.h" #include "../../lib/filesystem/Filesystem.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include -static double mbToBytes(double mb) -{ - return mb * 1024 * 1024; -} - void CModListView::setupModModel() { - modModel = new CModListModel(this); - manager = std::make_unique(modModel); + static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json"; + const auto &cachedRepositoryData = JsonUtils::jsonFromFile(repositoryCachePath); + + modStateModel = std::make_shared(); + if (!cachedRepositoryData.isNull()) + modStateModel->appendRepositories(cachedRepositoryData); + + modModel = new ModStateItemModel(modStateModel, this); + manager = std::make_unique(modStateModel); } void CModListView::changeEvent(QEvent *event) @@ -140,6 +144,7 @@ CModListView::CModListView(QWidget * parent) ui->splitter->setStyleSheet("QSplitter::handle {background: palette('window');}"); + disableModInfo(); setupModModel(); setupFilterModel(); setupModsView(); @@ -147,14 +152,9 @@ CModListView::CModListView(QWidget * parent) ui->progressWidget->setVisible(false); dlManager = nullptr; + modModel->reloadRepositories(); if(settings["launcher"]["autoCheckRepositories"].Bool()) - { loadRepositories(); - } - else - { - manager->resetRepositories(); - } #ifdef VCMI_MOBILE for(auto * scrollWidget : { @@ -171,8 +171,6 @@ CModListView::CModListView(QWidget * parent) void CModListView::loadRepositories() { - manager->resetRepositories(); - QStringList repositories; if (settings["launcher"]["defaultRepositoryEnabled"].Bool()) @@ -181,7 +179,7 @@ void CModListView::loadRepositories() if (settings["launcher"]["extraRepositoryEnabled"].Bool()) repositories.push_back(QString::fromStdString(settings["launcher"]["extraRepositoryURL"].String())); - for(auto entry : repositories) + for(const auto & entry : repositories) { if (entry.isEmpty()) continue; @@ -204,11 +202,21 @@ CModListView::~CModListView() static QString replaceIfNotEmpty(QVariant value, QString pattern) { - if(value.canConvert()) - return pattern.arg(value.toStringList().join(", ")); - if(value.canConvert()) - return pattern.arg(value.toString()); + { + if (value.toString().isEmpty()) + return ""; + else + return pattern.arg(value.toString()); + } + + if(value.canConvert()) + { + if (value.toStringList().isEmpty()) + return ""; + else + return pattern.arg(value.toStringList().join(", ")); + } // all valid types of data should have been filtered by code above assert(!value.isValid()); @@ -223,7 +231,7 @@ static QString replaceIfNotEmpty(QStringList value, QString pattern) return ""; } -QString CModListView::genChangelogText(CModEntry & mod) +QString CModListView::genChangelogText(const ModState & mod) { QString headerTemplate = "

%1:

"; QString entryBegin = "

    "; @@ -233,7 +241,7 @@ QString CModListView::genChangelogText(CModEntry & mod) QString result; - QVariantMap changelog = mod.getValue("changelog").toMap(); + QMap changelog = mod.getChangelog(); QList versions = changelog.keys(); std::sort(versions.begin(), versions.end(), [](QString lesser, QString greater) @@ -242,37 +250,59 @@ QString CModListView::genChangelogText(CModEntry & mod) }); std::reverse(versions.begin(), versions.end()); - for(auto & version : versions) + for(const auto & version : versions) { result += headerTemplate.arg(version); result += entryBegin; - for(auto & line : changelog.value(version).toStringList()) + for(const auto & line : changelog.value(version)) result += entryLine.arg(line); result += entryEnd; } return result; } -QStringList CModListView::getModNames(QStringList input) +QStringList CModListView::getModNames(QString queryingModID, QStringList input) { QStringList result; + auto queryingMod = modStateModel->getMod(queryingModID); + for(const auto & modID : input) { - auto mod = modModel->getMod(modID.toLower()); + if (modStateModel->isModExists(modID) && modStateModel->getMod(modID).isHidden()) + continue; - QString modName = mod.getValue("name").toString(); + QString parentModID = modStateModel->getTopParent(modID); + QString displayName; - if (modName.isEmpty()) - result += modID.toLower(); + if (modStateModel->isSubmod(modID) && queryingMod.getParentID() != parentModID ) + { + // show in form "parent mod (submod)" + + QString parentDisplayName = parentModID; + QString submodDisplayName = modID; + + if (modStateModel->isModExists(parentModID)) + parentDisplayName = modStateModel->getMod(parentModID).getName(); + + if (modStateModel->isModExists(modID)) + submodDisplayName = modStateModel->getMod(modID).getName(); + + displayName = QString("%1 (%2)").arg(submodDisplayName, parentDisplayName); + } else - result += modName; + { + // show simply as mod name + displayName = modID; + if (modStateModel->isModExists(modID)) + displayName = modStateModel->getMod(modID).getName(); + } + result += displayName; } - return result; } -QString CModListView::genModInfoText(CModEntry & mod) +QString CModListView::genModInfoText(const ModState & mod) { QString prefix = "

    %1: "; // shared prefix QString redPrefix = "

    %1: "; // shared prefix @@ -286,29 +316,40 @@ QString CModListView::genModInfoText(CModEntry & mod) QString result; - result += replaceIfNotEmpty(mod.getValue("name"), lineTemplate.arg(tr("Mod name"))); - result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg(tr("Installed version"))); - result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version"))); + result += replaceIfNotEmpty(mod.getName(), lineTemplate.arg(tr("Mod name"))); + if (mod.isUpdateAvailable()) + { + result += replaceIfNotEmpty(mod.getInstalledVersion(), lineTemplate.arg(tr("Installed version"))); + result += replaceIfNotEmpty(mod.getRepositoryVersion(), lineTemplate.arg(tr("Latest version"))); + } + else + { + if (mod.isInstalled()) + result += replaceIfNotEmpty(mod.getInstalledVersion(), lineTemplate.arg(tr("Installed version"))); + else + result += replaceIfNotEmpty(mod.getRepositoryVersion(), lineTemplate.arg(tr("Latest version"))); + } - if(mod.getValue("localSizeBytes").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSizeBytes").toDouble()), lineTemplate.arg(tr("Size"))); - if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("downloadSize").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mbToBytes(mod.getValue("downloadSize").toDouble())), lineTemplate.arg(tr("Download size"))); + if (mod.isInstalled()) + result += replaceIfNotEmpty(modStateModel->getInstalledModSizeFormatted(mod.getID()), lineTemplate.arg(tr("Size"))); + + if((!mod.isInstalled() || mod.isUpdateAvailable()) && !mod.getDownloadSizeFormatted().isEmpty()) + result += replaceIfNotEmpty(mod.getDownloadSizeFormatted(), lineTemplate.arg(tr("Download size"))); - result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors"))); + result += replaceIfNotEmpty(mod.getAuthors(), lineTemplate.arg(tr("Authors"))); - if(mod.getValue("licenseURL").isValid()) - result += urlTemplate.arg(tr("License")).arg(mod.getValue("licenseURL").toString()).arg(mod.getValue("licenseName").toString()); + if(!mod.getLicenseName().isEmpty()) + result += urlTemplate.arg(tr("License")).arg(mod.getLicenseUrl()).arg(mod.getLicenseName()); - if(mod.getValue("contact").isValid()) - result += urlTemplate.arg(tr("Contact")).arg(mod.getValue("contact").toString()).arg(mod.getValue("contact").toString()); + if(!mod.getContact().isEmpty()) + result += urlTemplate.arg(tr("Contact")).arg(mod.getContact()).arg(mod.getContact()); //compatibility info if(!mod.isCompatible()) { - auto compatibilityInfo = mod.getValue("compatibility").toMap(); - auto minStr = compatibilityInfo.value("min").toString(); - auto maxStr = compatibilityInfo.value("max").toString(); + auto compatibilityInfo = mod.getCompatibleVersionRange(); + auto minStr = compatibilityInfo.first; + auto maxStr = compatibilityInfo.second; result += incompatibleString.arg(tr("Compatibility")); if(minStr == maxStr) @@ -327,52 +368,57 @@ QString CModListView::genModInfoText(CModEntry & mod) } } - QStringList supportedLanguages; - QVariant baseLanguageVariant = mod.getBaseValue("language"); + QVariant baseLanguageVariant = mod.getBaseLanguage(); QString baseLanguageID = baseLanguageVariant.isValid() ? baseLanguageVariant.toString() : "english"; - bool needToShowSupportedLanguages = false; + QStringList supportedLanguages = mod.getSupportedLanguages(); - for(const auto & language : Languages::getLanguageList()) + if(supportedLanguages.size() > 1) { - QString languageID = QString::fromStdString(language.identifier); + QStringList supportedLanguagesTranslated; - if (languageID != baseLanguageID && !mod.getValue(languageID).isValid()) - continue; + for (const auto & languageID : supportedLanguages) + supportedLanguagesTranslated += QApplication::translate("Language", Languages::getLanguageOptions(languageID.toStdString()).nameEnglish.c_str()); - if (languageID != baseLanguageID) - needToShowSupportedLanguages = true; - - supportedLanguages += QApplication::translate("Language", language.nameEnglish.c_str()); + result += replaceIfNotEmpty(supportedLanguagesTranslated, lineTemplate.arg(tr("Languages"))); } - if(needToShowSupportedLanguages) - result += replaceIfNotEmpty(supportedLanguages, lineTemplate.arg(tr("Languages"))); + QStringList conflicts = mod.getConflicts(); + for (const auto & otherMod : modStateModel->getAllMods()) + { + QStringList otherConflicts = modStateModel->getMod(otherMod).getConflicts(); - result += replaceIfNotEmpty(getModNames(mod.getDependencies()), lineTemplate.arg(tr("Required mods"))); - result += replaceIfNotEmpty(getModNames(mod.getConflicts()), lineTemplate.arg(tr("Conflicting mods"))); - result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg(tr("Description"))); + if (otherConflicts.contains(mod.getID()) && !conflicts.contains(otherMod)) + conflicts.push_back(otherMod); + } + + result += replaceIfNotEmpty(getModNames(mod.getID(), mod.getDependencies()), lineTemplate.arg(tr("Required mods"))); + result += replaceIfNotEmpty(getModNames(mod.getID(), conflicts), lineTemplate.arg(tr("Conflicting mods"))); + result += replaceIfNotEmpty(mod.getDescription(), textTemplate.arg(tr("Description"))); result += "

    "; // to get some empty space - QString unknownDeps = tr("This mod can not be installed or enabled because the following dependencies are not present"); - QString blockingMods = tr("This mod can not be enabled because the following mods are incompatible with it"); - QString hasActiveDependentMods = tr("This mod cannot be disabled because it is required by the following mods"); - QString hasDependentMods = tr("This mod cannot be uninstalled or updated because it is required by the following mods"); + QString translationMismatch = tr("This mod cannot be enabled because it translates into a different language."); + QString notInstalledDeps = tr("This mod can not be enabled because the following dependencies are not present"); + QString unavailableDeps = tr("This mod can not be installed because the following dependencies are not present"); QString thisIsSubmod = tr("This is a submod and it cannot be installed or uninstalled separately from its parent mod"); QString notes; - notes += replaceIfNotEmpty(getModNames(findInvalidDependencies(mod.getName())), listTemplate.arg(unknownDeps)); - notes += replaceIfNotEmpty(getModNames(findBlockingMods(mod.getName())), listTemplate.arg(blockingMods)); - if(mod.isEnabled()) - notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), true)), listTemplate.arg(hasActiveDependentMods)); - if(mod.isInstalled()) - notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), false)), listTemplate.arg(hasDependentMods)); + QStringList notInstalledDependencies = this->getModsToInstall(mod.getID()); + QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies); + + if (mod.isInstalled()) + notes += replaceIfNotEmpty(getModNames(mod.getID(), notInstalledDependencies), listTemplate.arg(notInstalledDeps)); + else + notes += replaceIfNotEmpty(getModNames(mod.getID(), unavailableDependencies), listTemplate.arg(unavailableDeps)); if(mod.isSubmod()) notes += noteTemplate.arg(thisIsSubmod); + if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString()) + notes += noteTemplate.arg(translationMismatch); + if(notes.size()) result += textTemplate.arg(tr("Notes")).arg(notes); @@ -395,6 +441,8 @@ void CModListView::dataChanged(const QModelIndex & topleft, const QModelIndex & void CModListView::selectMod(const QModelIndex & index) { + ui->tabWidget->setCurrentIndex(0); + if(!index.isValid()) { disableModInfo(); @@ -402,7 +450,10 @@ void CModListView::selectMod(const QModelIndex & index) else { const auto modName = index.data(ModRoles::ModNameRole).toString(); - auto mod = modModel->getMod(modName); + auto mod = modStateModel->getMod(modName); + + ui->tabWidget->setTabEnabled(1, !mod.getChangelog().isEmpty()); + ui->tabWidget->setTabEnabled(2, !mod.getScreenshots().isEmpty()); ui->modInfoBrowser->setHtml(genModInfoText(mod)); ui->changelogBrowser->setHtml(genChangelogText(mod)); @@ -410,24 +461,22 @@ void CModListView::selectMod(const QModelIndex & index) Helper::enableScrollBySwiping(ui->modInfoBrowser); Helper::enableScrollBySwiping(ui->changelogBrowser); - bool hasInvalidDeps = !findInvalidDependencies(modName).empty(); - bool hasBlockingMods = !findBlockingMods(modName).empty(); - bool hasDependentMods = !findDependentMods(modName, true).empty(); + QStringList notInstalledDependencies = this->getModsToInstall(modName); + QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies); + bool translationMismatch = mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString(); - ui->disableButton->setVisible(mod.isEnabled()); - ui->enableButton->setVisible(mod.isDisabled()); + ui->disableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && modStateModel->isModEnabled(mod.getID())); + ui->enableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && !modStateModel->isModEnabled(mod.getID())); ui->installButton->setVisible(mod.isAvailable() && !mod.isSubmod()); ui->uninstallButton->setVisible(mod.isInstalled() && !mod.isSubmod()); - ui->updateButton->setVisible(mod.isUpdateable()); + ui->updateButton->setVisible(mod.isUpdateAvailable()); // Block buttons if action is not allowed at this time - // TODO: automate handling of some of these cases instead of forcing player - // to resolve all conflicts manually. - ui->disableButton->setEnabled(!hasDependentMods && !mod.isEssential()); - ui->enableButton->setEnabled(!hasBlockingMods && !hasInvalidDeps); - ui->installButton->setEnabled(!hasInvalidDeps); - ui->uninstallButton->setEnabled(!hasDependentMods && !mod.isEssential()); - ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods); + ui->disableButton->setEnabled(true); + ui->enableButton->setEnabled(notInstalledDependencies.empty() && !translationMismatch); + ui->installButton->setEnabled(unavailableDependencies.empty()); + ui->uninstallButton->setEnabled(true); + ui->updateButton->setEnabled(unavailableDependencies.empty()); loadScreenshots(); } @@ -460,149 +509,122 @@ void CModListView::on_lineEdit_textChanged(const QString & arg1) void CModListView::on_comboBox_currentIndexChanged(int index) { - switch(index) - { - case 0: - filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::MASK_NONE); - break; - case 1: - filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::INSTALLED); - break; - case 2: - filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::INSTALLED); - break; - case 3: - filterModel->setTypeFilter(ModStatus::UPDATEABLE, ModStatus::UPDATEABLE); - break; - case 4: - filterModel->setTypeFilter(ModStatus::ENABLED | ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED); - break; - case 5: - filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED); - break; - } + auto enumIndex = static_cast(index); + filterModel->setTypeFilter(enumIndex); } -QStringList CModListView::findInvalidDependencies(QString mod) +QStringList CModListView::findUnavailableMods(QStringList candidates) { - QStringList ret; - for(QString requirement : modModel->getRequirements(mod)) + QStringList invalidMods; + + for(QString modName : candidates) { - if(!modModel->hasMod(requirement) && !modModel->hasMod(requirement.split(QChar('.'))[0])) - ret += requirement; + if(!modStateModel->isModExists(modName)) + invalidMods.push_back(modName); } - return ret; -} - -QStringList CModListView::findBlockingMods(QString modUnderTest) -{ - QStringList ret; - auto required = modModel->getRequirements(modUnderTest); - - for(QString name : modModel->getModList()) - { - auto mod = modModel->getMod(name); - - if(mod.isEnabled()) - { - // one of enabled mods have requirement (or this mod) marked as conflict - for(auto conflict : mod.getConflicts()) - { - if(required.contains(conflict)) - ret.push_back(name); - } - } - } - - return ret; -} - -QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled) -{ - QStringList ret; - for(QString modName : modModel->getModList()) - { - auto current = modModel->getMod(modName); - - if(!current.isInstalled() || !current.isVisible()) - continue; - - if(current.getDependencies().contains(mod, Qt::CaseInsensitive)) - { - if(!(current.isDisabled() && excludeDisabled)) - ret += modName; - } - } - return ret; + return invalidMods; } void CModListView::on_enableButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - enableModByName(modName); - checkManagerErrors(); } void CModListView::enableModByName(QString modName) { - assert(findBlockingMods(modName).empty()); - assert(findInvalidDependencies(modName).empty()); - - for(auto & name : modModel->getRequirements(modName)) - { - if(modModel->getMod(name).isDisabled()) - manager->enableMod(name); - } - emit modsChanged(); + manager->enableMods({modName}); + modModel->modChanged(modName); } void CModListView::on_disableButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - disableModByName(modName); - checkManagerErrors(); } void CModListView::disableModByName(QString modName) { - if(modModel->hasMod(modName) && modModel->getMod(modName).isEnabled()) - manager->disableMod(modName); + manager->disableMod(modName); + modModel->modChanged(modName); +} - emit modsChanged(); +QStringList CModListView::getModsToInstall(QString mod) +{ + QStringList result; + QStringList candidates; + QStringList processed; + + candidates.push_back(mod); + while (!candidates.empty()) + { + QString potentialToInstall = candidates.back(); + candidates.pop_back(); + processed.push_back(potentialToInstall); + + if (modStateModel->isModExists(potentialToInstall) && modStateModel->isModInstalled(potentialToInstall)) + continue; + + if (modStateModel->isSubmod(potentialToInstall)) + { + QString topParent = modStateModel->getTopParent(potentialToInstall); + if (modStateModel->isModInstalled(topParent)) + { + if (modStateModel->isModUpdateAvailable(topParent)) + potentialToInstall = modStateModel->getTopParent(potentialToInstall); + // else - potentially broken mod that depends on non-existing submod + } + else + potentialToInstall = modStateModel->getTopParent(potentialToInstall); + } + + result.push_back(potentialToInstall); + + if (modStateModel->isModExists(potentialToInstall)) + { + QStringList dependencies = modStateModel->getMod(potentialToInstall).getDependencies(); + for (const auto & dependency : dependencies) + { + if (!processed.contains(dependency) && !candidates.contains(dependency)) + candidates.push_back(dependency); + } + } + } + result.removeDuplicates(); + return result; } void CModListView::on_updateButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); + auto targetMod = modStateModel->getMod(modName); - assert(findInvalidDependencies(modName).empty()); + if(targetMod.isUpdateAvailable()) + downloadFile(modName + ".zip", targetMod.getDownloadUrl(), modName, targetMod.getDownloadSizeBytes()); - for(auto & name : modModel->getRequirements(modName)) + for(const auto & name : getModsToInstall(modName)) { - auto mod = modModel->getMod(name); + auto mod = modStateModel->getMod(name); // update required mod, install missing (can be new dependency) - if(mod.isUpdateable() || !mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), name, mbToBytes(mod.getValue("downloadSize").toDouble())); + if(mod.isUpdateAvailable() || !mod.isInstalled()) + downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes()); } } void CModListView::on_uninstallButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - // NOTE: perhaps add "manually installed" flag and uninstall those dependencies that don't have it? - if(modModel->hasMod(modName) && modModel->getMod(modName).isInstalled()) + if(modStateModel->isModExists(modName) && modStateModel->getMod(modName).isInstalled()) { - if(modModel->getMod(modName).isEnabled()) + if(modStateModel->isModEnabled(modName)) manager->disableMod(modName); manager->uninstallMod(modName); + modModel->reloadRepositories(); } - emit modsChanged(); checkManagerErrors(); } @@ -610,28 +632,14 @@ void CModListView::on_installButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - assert(findInvalidDependencies(modName).empty()); - - for(auto & name : modModel->getRequirements(modName)) + for(const auto & name : getModsToInstall(modName)) { - auto mod = modModel->getMod(name); + auto mod = modStateModel->getMod(name); if(mod.isAvailable()) - downloadFile(name + ".zip", mod.getValue("download").toString(), name, mbToBytes(mod.getValue("downloadSize").toDouble())); - else if(!mod.isEnabled()) + downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes()); + else if(!modStateModel->isModEnabled(name)) enableModByName(name); } - - for(auto & name : modModel->getMod(modName).getConflicts()) - { - auto mod = modModel->getMod(name); - if(mod.isEnabled()) - { - //TODO: consider reverse dependencies disabling - //TODO: consider if it may be possible for subdependencies to block disabling conflicting mod? - //TODO: consider if it may be possible to get subconflicts that will block disabling conflicting mod? - disableModByName(name); - } - } } void CModListView::on_installFromFileButton_clicked() @@ -683,11 +691,12 @@ void CModListView::manualInstallFile(QString filePath) // reload settings Helper::loadSettings(); - for(auto widget : qApp->allWidgets()) + for(const auto widget : qApp->allWidgets()) if(auto settingsView = qobject_cast(widget)) settingsView->loadSettings(); - manager->loadMods(); - manager->loadModSettings(); + + modStateModel->reloadLocalState(); + modModel->reloadRepositories(); } } } @@ -695,12 +704,12 @@ void CModListView::manualInstallFile(QString filePath) downloadFile(fileName, QUrl::fromLocalFile(filePath), fileName); } -void CModListView::downloadFile(QString file, QString url, QString description, qint64 size) +void CModListView::downloadFile(QString file, QString url, QString description, qint64 sizeBytes) { - downloadFile(file, QUrl{url}, description, size); + downloadFile(file, QUrl{url}, description, sizeBytes); } -void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 size) +void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes) { if(!dlManager) { @@ -715,13 +724,13 @@ void CModListView::downloadFile(QString file, QUrl url, QString description, qin connect(manager.get(), SIGNAL(extractionProgress(qint64,qint64)), this, SLOT(extractionProgress(qint64,qint64))); - connect(modModel, &CModListModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged); + connect(modModel, &ModStateItemModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged); const auto progressBarFormat = tr("Downloading %1. %p% (%v MB out of %m MB) finished").arg(description); ui->progressBar->setFormat(progressBarFormat); } - dlManager->downloadFile(url, file, size); + dlManager->downloadFile(url, file, sizeBytes); } void CModListView::downloadProgress(qint64 current, qint64 max) @@ -780,7 +789,6 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi installFiles(savedFiles); hideProgressBar(); - emit modsChanged(); } void CModListView::hideProgressBar() @@ -799,7 +807,7 @@ void CModListView::installFiles(QStringList files) QStringList maps; QStringList images; QStringList exe; - QVector repositories; + JsonNode repository; // TODO: some better way to separate zip's with mods and downloaded repository files for(QString filename : files) @@ -813,36 +821,47 @@ void CModListView::installFiles(QStringList files) else if(filename.endsWith(".json", Qt::CaseInsensitive)) { //download and merge additional files - auto repoData = JsonUtils::JsonFromFile(filename).toMap(); - if(repoData.value("name").isNull()) + const auto &repoData = JsonUtils::jsonFromFile(filename); + if(repoData["name"].isNull()) { - for(const auto & key : repoData.keys()) + // This is main repository index. Download all referenced mods + for(const auto & [modName, modJson] : repoData.Struct()) { - auto modjson = repoData[key].toMap().value("mod"); - if(!modjson.isNull()) - { - downloadFile(key + ".json", modjson.toString(), tr("mods repository index")); - } + auto modNameLower = boost::algorithm::to_lower_copy(modName); + auto modJsonUrl = modJson["mod"]; + if(!modJsonUrl.isNull()) + downloadFile(QString::fromStdString(modName + ".json"), QString::fromStdString(modJsonUrl.String()), tr("mods repository index")); + + repository[modNameLower] = modJson; } } else { - auto modn = QFileInfo(filename).baseName(); - QVariantMap temp; - temp[modn] = repoData; - repoData = temp; + // This is json of a single mod. Extract name of mod and add it to repo + auto modName = QFileInfo(filename).baseName().toStdString(); + auto modNameLower = boost::algorithm::to_lower_copy(modName); + repository[modNameLower] = repoData; } - repositories.push_back(repoData); } else if(filename.endsWith(".png", Qt::CaseInsensitive)) images.push_back(filename); } - if (!repositories.empty()) - manager->loadRepositories(repositories); + if (!repository.isNull()) + { + manager->appendRepositories(repository); + modModel->reloadRepositories(); + + static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json"; + JsonUtils::jsonToFile(repositoryCachePath, modStateModel->getRepositoryData()); + } if(!mods.empty()) + { installMods(mods); + modStateModel->reloadLocalState(); + modModel->reloadRepositories(); + } if(!maps.empty()) installMaps(maps); @@ -869,10 +888,8 @@ void CModListView::installFiles(QStringList files) if(futureExtract.get()) { //update - CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &){ return true; }); - manager->loadMods(); + modStateModel->reloadLocalState(); modModel->reloadRepositories(); - emit modsChanged(); } } @@ -883,6 +900,7 @@ void CModListView::installFiles(QStringList files) void CModListView::installMods(QStringList archives) { QStringList modNames; + QStringList modsToEnable; for(QString archive : archives) { @@ -893,65 +911,31 @@ void CModListView::installMods(QStringList archives) modNames.push_back(modName); } - QStringList modsToEnable; - - // disable mod(s), to properly recalculate dependencies, if changed - for(QString mod : boost::adaptors::reverse(modNames)) + // uninstall old version of mod, if installed + for(QString mod : modNames) { - CModEntry entry = modModel->getMod(mod); - if(entry.isInstalled()) + if(modStateModel->getMod(mod).isInstalled()) { - // enable mod if installed and enabled - if(entry.isEnabled()) + if (modStateModel->isModEnabled(mod)) modsToEnable.push_back(mod); + + manager->uninstallMod(mod); } else { - // enable mod if m - if(settings["launcher"]["enableInstalledMods"].Bool()) - modsToEnable.push_back(mod); + // installation of previously not present mod -> enable it + modsToEnable.push_back(mod); } } - // uninstall old version of mod, if installed - for(QString mod : boost::adaptors::reverse(modNames)) - { - if(modModel->getMod(mod).isInstalled()) - manager->uninstallMod(mod); - } - for(int i = 0; i < modNames.size(); i++) { ui->progressBar->setFormat(tr("Installing mod %1").arg(modNames[i])); manager->installMod(modNames[i], archives[i]); } - std::function enableMod; - - enableMod = [&](QString modName) - { - auto mod = modModel->getMod(modName); - if(mod.isInstalled() && !mod.getValue("keepDisabled").toBool()) - { - for (auto const & dependencyName : mod.getDependencies()) - { - auto dependency = modModel->getMod(dependencyName); - if(dependency.isDisabled()) - manager->enableMod(dependencyName); - } - - if(mod.isDisabled() && manager->enableMod(modName)) - { - for(QString child : modModel->getChildren(modName)) - enableMod(child); - } - } - }; - - for(QString mod : modsToEnable) - { - enableMod(mod); - } + if (!modsToEnable.empty()) + manager->enableMods(modsToEnable); checkManagerErrors(); @@ -1014,9 +998,9 @@ void CModListView::loadScreenshots() ui->screenshotsList->clear(); QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - assert(modModel->hasMod(modName)); //should be filtered out by check above + assert(modStateModel->isModExists(modName)); //should be filtered out by check above - for(QString url : modModel->getMod(modName).getValue("screenshots").toStringList()) + for(QString url : modStateModel->getMod(modName).getScreenshots()) { // URL must be encoded to something else to get rid of symbols illegal in file names const auto hashed = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5); @@ -1050,52 +1034,42 @@ void CModListView::on_screenshotsList_clicked(const QModelIndex & index) } } -const CModList & CModListView::getModList() const -{ - assert(modModel); - return *modModel; -} - void CModListView::doInstallMod(const QString & modName) { - assert(findInvalidDependencies(modName).empty()); - - for(auto & name : modModel->getRequirements(modName)) + for(const auto & name : modStateModel->getMod(modName).getDependencies()) { - auto mod = modModel->getMod(name); + auto mod = modStateModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), name, mbToBytes(mod.getValue("downloadSize").toDouble())); + downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes()); } } bool CModListView::isModAvailable(const QString & modName) { - auto mod = modModel->getMod(modName); - return mod.isAvailable(); + return !modStateModel->isModInstalled(modName); } bool CModListView::isModEnabled(const QString & modName) { - auto mod = modModel->getMod(modName); - return mod.isEnabled(); + return modStateModel->isModEnabled(modName); } bool CModListView::isModInstalled(const QString & modName) { - auto mod = modModel->getMod(modName); + auto mod = modStateModel->getMod(modName); return mod.isInstalled(); } QString CModListView::getTranslationModName(const QString & language) { - for(const auto & modName : modModel->getModList()) + for(const auto & modName : modStateModel->getAllMods()) { - auto mod = modModel->getMod(modName); + auto mod = modStateModel->getMod(modName); if (!mod.isTranslation()) continue; - if (mod.getBaseValue("language").toString() != language) + if (mod.getBaseLanguage() != language) continue; return modName; @@ -1110,19 +1084,18 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) return; auto modName = index.data(ModRoles::ModNameRole).toString(); - auto mod = modModel->getMod(modName); + auto mod = modStateModel->getMod(modName); - bool hasInvalidDeps = !findInvalidDependencies(modName).empty(); - bool hasBlockingMods = !findBlockingMods(modName).empty(); - bool hasDependentMods = !findDependentMods(modName, true).empty(); - - if(!hasInvalidDeps && mod.isAvailable() && !mod.isSubmod()) + QStringList notInstalledDependencies = this->getModsToInstall(mod.getID()); + QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies); + + if(unavailableDependencies.empty() && mod.isAvailable() && !mod.isSubmod()) { on_installButton_clicked(); return; } - if(!hasInvalidDeps && !hasDependentMods && mod.isUpdateable() && index.column() == ModFields::STATUS_UPDATE) + if(unavailableDependencies.empty() && mod.isUpdateAvailable() && index.column() == ModFields::STATUS_UPDATE) { on_updateButton_clicked(); return; @@ -1138,13 +1111,13 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) return; } - if(!hasBlockingMods && !hasInvalidDeps && mod.isDisabled()) + if(notInstalledDependencies.empty() && !modStateModel->isModEnabled(modName)) { on_enableButton_clicked(); return; } - if(!hasDependentMods && !mod.isEssential() && mod.isEnabled()) + if(modStateModel->isModEnabled(modName)) { on_disableButton_clicked(); return; diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index 595680bf5..c958c2aff 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -17,21 +17,23 @@ namespace Ui class CModListView; } -class CModManager; +class ModStateController; class CModList; -class CModListModel; +class ModStateItemModel; +class ModStateModel; class CModFilterModel; class CDownloadManager; class QTableWidgetItem; -class CModEntry; +class ModState; class CModListView : public QWidget { Q_OBJECT - std::unique_ptr manager; - CModListModel * modModel; + std::shared_ptr modStateModel; + std::unique_ptr manager; + ModStateItemModel * modModel; CModFilterModel * filterModel; CDownloadManager * dlManager; @@ -42,31 +44,28 @@ class CModListView : public QWidget void checkManagerErrors(); /// replace mod ID's with proper human-readable mod names - QStringList getModNames(QStringList input); + QStringList getModNames(QString queryingMod, QStringList input); + + /// returns list of mods that are needed for install of this mod (potentially including this mod itself) + QStringList getModsToInstall(QString mod); // find mods unknown to mod list (not present in repo and not installed) - QStringList findInvalidDependencies(QString mod); - // find mods that block enabling of this mod: conflicting with this mod or one of required mods - QStringList findBlockingMods(QString modUnderTest); - // find mods that depend on this one - QStringList findDependentMods(QString mod, bool excludeDisabled); + QStringList findUnavailableMods(QStringList candidates); void manualInstallFile(QString filePath); - void downloadFile(QString file, QString url, QString description, qint64 size = 0); - void downloadFile(QString file, QUrl url, QString description, qint64 size = 0); + void downloadFile(QString file, QString url, QString description, qint64 sizeBytes = 0); + void downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes = 0); void installMods(QStringList archives); void installMaps(QStringList maps); void installFiles(QStringList mods); - QString genChangelogText(CModEntry & mod); - QString genModInfoText(CModEntry & mod); + QString genChangelogText(const ModState & mod); + QString genModInfoText(const ModState & mod); void changeEvent(QEvent *event) override; void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent *event) override; -signals: - void modsChanged(); public: explicit CModListView(QWidget * parent = nullptr); @@ -79,8 +78,6 @@ public: void selectMod(const QModelIndex & index); - const CModList & getModList() const; - // First Launch View interface /// install mod by name diff --git a/launcher/modManager/modstate.cpp b/launcher/modManager/modstate.cpp new file mode 100644 index 000000000..61b17b613 --- /dev/null +++ b/launcher/modManager/modstate.cpp @@ -0,0 +1,231 @@ +/* + * modstate.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "modstate.h" + +#include "../../lib/modding/ModDescription.h" +#include "../../lib/json/JsonNode.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/Languages.h" + +ModState::ModState(const ModDescription & impl) + : impl(impl) +{ +} + +QString ModState::getName() const +{ + return QString::fromStdString(impl.getName()); +} + +QString ModState::getType() const +{ + return QString::fromStdString(impl.getValue("modType").String()); +} + +QString ModState::getDescription() const +{ + return QString::fromStdString(impl.getLocalizedValue("description").String()); +} + +QString ModState::getID() const +{ + return QString::fromStdString(impl.getID()); +} + +QString ModState::getParentID() const +{ + return QString::fromStdString(impl.getParentID()); +} + +QString ModState::getTopParentID() const +{ + return QString::fromStdString(impl.getTopParentID()); +} + +template +QStringList stringListStdToQt(const Container & container) +{ + QStringList result; + for (const auto & str : container) + result.push_back(QString::fromStdString(str)); + return result; +} + +QStringList ModState::getDependencies() const +{ + return stringListStdToQt(impl.getDependencies()); +} + +QStringList ModState::getConflicts() const +{ + return stringListStdToQt(impl.getConflicts()); +} + +QStringList ModState::getScreenshots() const +{ + return stringListStdToQt(impl.getLocalizedValue("screenshots").convertTo>()); +} + +QString ModState::getBaseLanguage() const +{ + return QString::fromStdString(impl.getBaseLanguage()); +} + +QStringList ModState::getSupportedLanguages() const +{ + QStringList result; + result.push_back(getBaseLanguage()); + + for (const auto & language : Languages::getLanguageList()) + { + QString languageID = QString::fromStdString(language.identifier); + + if (languageID != getBaseLanguage() && !impl.getValue(language.identifier).isNull()) + result.push_back(languageID); + } + return result; +} + +QMap ModState::getChangelog() const +{ + QMap result; + const JsonNode & changelog = impl.getLocalizedValue("changelog"); + + for (const auto & entry : changelog.Struct()) + { + QString version = QString::fromStdString(entry.first); + QStringList changes = stringListStdToQt(entry.second.convertTo>()); + + result[version] = changes; + } + + return result; +} + +QString ModState::getInstalledVersion() const +{ + return QString::fromStdString(impl.getLocalValue("version").String()); +} + +QString ModState::getRepositoryVersion() const +{ + return QString::fromStdString(impl.getRepositoryValue("version").String()); +} + +QString ModState::getVersion() const +{ + return QString::fromStdString(impl.getValue("version").String()); +} + +double ModState::getDownloadSizeMegabytes() const +{ + return impl.getRepositoryValue("downloadSize").Float(); +} + +size_t ModState::getDownloadSizeBytes() const +{ + return getDownloadSizeMegabytes() * 1024 * 1024; +} + +QString ModState::getDownloadSizeFormatted() const +{ + return QCoreApplication::translate("File size", "%1 MiB").arg(QString::number(getDownloadSizeMegabytes(), 'f', 1)); +} + +QString ModState::getAuthors() const +{ + return QString::fromStdString(impl.getLocalizedValue("author").String()); +} + +QString ModState::getContact() const +{ + return QString::fromStdString(impl.getValue("contact").String()); +} + +QString ModState::getLicenseUrl() const +{ + return QString::fromStdString(impl.getValue("licenseURL").String()); +} + +QString ModState::getLicenseName() const +{ + return QString::fromStdString(impl.getValue("licenseName").String()); +} + +QString ModState::getDownloadUrl() const +{ + return QString::fromStdString(impl.getRepositoryValue("download").String()); +} + +QPair ModState::getCompatibleVersionRange() const +{ + const JsonNode & compatibility = impl.getValue("compatibility"); + + if (compatibility.isNull()) + return {}; + + auto min = QString::fromStdString(compatibility["min"].String()); + auto max = QString::fromStdString(compatibility["max"].String()); + return { min, max}; +} + +bool ModState::isSubmod() const +{ + return !getParentID().isEmpty(); +} + +bool ModState::isCompatibility() const +{ + return impl.isCompatibility(); +} + +bool ModState::isTranslation() const +{ + return impl.isTranslation(); +} + +bool ModState::isVisible() const +{ + return !isHidden(); +} + +bool ModState::isHidden() const +{ + if (isTranslation() && !isInstalled()) + return impl.getBaseLanguage() != CGeneralTextHandler::getPreferredLanguage(); + + return isCompatibility() || getID() == "vcmi" || getID() == "core"; +} + +bool ModState::isAvailable() const +{ + return !isInstalled(); +} + +bool ModState::isInstalled() const +{ + return impl.isInstalled(); +} + +bool ModState::isUpdateAvailable() const +{ + return impl.isUpdateAvailable(); +} + +bool ModState::isCompatible() const +{ + return impl.isCompatible(); +} + +bool ModState::isKeptDisabled() const +{ + return impl.keepDisabled(); +} diff --git a/launcher/modManager/modstate.h b/launcher/modManager/modstate.h new file mode 100644 index 000000000..fb438f078 --- /dev/null +++ b/launcher/modManager/modstate.h @@ -0,0 +1,69 @@ +/* + * modstate.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN +class ModDescription; +VCMI_LIB_NAMESPACE_END + +/// Class that represent current state of mod in Launcher +/// Provides Qt-based interface to library class ModDescription +class ModState +{ + const ModDescription & impl; + +public: + explicit ModState(const ModDescription & impl); + + QString getName() const; + QString getType() const; + QString getDescription() const; + + QString getID() const; + QString getParentID() const; + QString getTopParentID() const; + + QStringList getDependencies() const; + QStringList getConflicts() const; + QStringList getScreenshots() const; + + QString getBaseLanguage() const; + QStringList getSupportedLanguages() const; + + QMap getChangelog() const; + + QString getInstalledVersion() const; + QString getRepositoryVersion() const; + QString getVersion() const; + + double getDownloadSizeMegabytes() const; + size_t getDownloadSizeBytes() const; + QString getDownloadSizeFormatted() const; + QString getAuthors() const; + QString getContact() const; + QString getLicenseUrl() const; + QString getLicenseName() const; + + QString getDownloadUrl() const; + + QPair getCompatibleVersionRange() const; + + bool isSubmod() const; + bool isCompatibility() const; + bool isTranslation() const; + + bool isVisible() const; + bool isHidden() const; + bool isAvailable() const; + bool isInstalled() const; + bool isUpdateAvailable() const; + bool isCompatible() const; + bool isKeptDisabled() const; +}; diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/modstatecontroller.cpp similarity index 53% rename from launcher/modManager/cmodmanager.cpp rename to launcher/modManager/modstatecontroller.cpp index 4288f4a15..fa243f0b8 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/modstatecontroller.cpp @@ -1,5 +1,5 @@ /* - * cmodmanager.cpp, part of VCMI engine + * modstatecontroller.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -8,14 +8,17 @@ * */ #include "StdInc.h" -#include "cmodmanager.h" +#include "modstatecontroller.h" + +#include "modstatemodel.h" #include "../../lib/VCMIDirs.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CZipLoader.h" #include "../../lib/modding/CModHandler.h" -#include "../../lib/modding/CModInfo.h" #include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/json/JsonNode.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../vcmiqt/jsonutils.h" #include "../vcmiqt/launcherdirs.h" @@ -40,7 +43,7 @@ QString detectModArchive(QString path, QString modName, std::vector for(int folderLevel : {0, 1}) //search in subfolder if there is no mod.json in the root { - for(auto file : filesToExtract) + for(const auto & file : filesToExtract) { QString filename = QString::fromUtf8(file.c_str()); modDirName = filename.section('/', 0, folderLevel); @@ -54,7 +57,7 @@ QString detectModArchive(QString path, QString modName, std::vector logGlobal->error("Failed to detect mod path in archive!"); logGlobal->debug("List of file in archive:"); - for(auto file : filesToExtract) + for(const auto & file : filesToExtract) logGlobal->debug("%s", file.c_str()); return ""; @@ -62,105 +65,60 @@ QString detectModArchive(QString path, QString modName, std::vector } -CModManager::CModManager(CModList * modList) +ModStateController::ModStateController(std::shared_ptr modList) : modList(modList) { - loadMods(); - loadModSettings(); } -QString CModManager::settingsPath() +ModStateController::~ModStateController() = default; + +void ModStateController::appendRepositories(const JsonNode & repomap) { - return pathToQString(VCMIDirs::get().userConfigPath() / "modSettings.json"); + modList->appendRepositories(repomap); } -void CModManager::loadModSettings() -{ - modSettings = JsonUtils::JsonFromFile(settingsPath()).toMap(); - modList->setModSettings(modSettings["activeMods"]); -} - -void CModManager::resetRepositories() -{ - modList->resetRepositories(); -} - -void CModManager::loadRepositories(QVector repomap) -{ - for (auto const & entry : repomap) - modList->addRepository(entry); - modList->reloadRepositories(); -} - -void CModManager::loadMods() -{ - CModHandler handler; - handler.loadMods(); - auto installedMods = handler.getAllMods(); - localMods.clear(); - - for(auto modname : installedMods) - { - auto resID = CModInfo::getModFile(modname); - if(CResourceHandler::get()->existsResource(resID)) - { - //calculate mod size - qint64 total = 0; - ResourcePath resDir(CModInfo::getModDir(modname), EResType::DIRECTORY); - if(CResourceHandler::get()->existsResource(resDir)) - { - for(QDirIterator iter(QString::fromStdString(CResourceHandler::get()->getResourceName(resDir)->string()), QDirIterator::Subdirectories); iter.hasNext(); iter.next()) - total += iter.fileInfo().size(); - } - - boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID); - auto mod = JsonUtils::JsonFromFile(pathToQString(name)); - auto json = JsonUtils::toJson(mod); - json["localSizeBytes"].Float() = total; - if(!name.is_absolute()) - json["storedLocally"].Bool() = true; - - mod = JsonUtils::toVariant(json); - localMods.insert(QString::fromUtf8(modname.c_str()).toLower(), mod); - } - } - modList->setLocalModList(localMods); -} - -bool CModManager::addError(QString modname, QString message) +bool ModStateController::addError(QString modname, QString message) { recentErrors.push_back(QString("%1: %2").arg(modname).arg(message)); return false; } -QStringList CModManager::getErrors() +QStringList ModStateController::getErrors() { QStringList ret = recentErrors; recentErrors.clear(); return ret; } -bool CModManager::installMod(QString modname, QString archivePath) +bool ModStateController::installMod(QString modname, QString archivePath) { return canInstallMod(modname) && doInstallMod(modname, archivePath); } -bool CModManager::uninstallMod(QString modname) +bool ModStateController::uninstallMod(QString modname) { return canUninstallMod(modname) && doUninstallMod(modname); } -bool CModManager::enableMod(QString modname) +bool ModStateController::enableMods(QStringList modlist) { - return canEnableMod(modname) && doEnableMod(modname, true); + for (const auto & modname : modlist) + if (!canEnableMod(modname)) + return false; + + modList->doEnableMods(modlist); + return true; } -bool CModManager::disableMod(QString modname) +bool ModStateController::disableMod(QString modname) { - return canDisableMod(modname) && doEnableMod(modname, false); + if (!canDisableMod(modname)) + return false; + modList->doDisableMod(modname); + return true; } -bool CModManager::canInstallMod(QString modname) +bool ModStateController::canInstallMod(QString modname) { auto mod = modList->getMod(modname); @@ -172,7 +130,7 @@ bool CModManager::canInstallMod(QString modname) return true; } -bool CModManager::canUninstallMod(QString modname) +bool ModStateController::canUninstallMod(QString modname) { auto mod = modList->getMod(modname); @@ -185,11 +143,11 @@ bool CModManager::canUninstallMod(QString modname) return true; } -bool CModManager::canEnableMod(QString modname) +bool ModStateController::canEnableMod(QString modname) { auto mod = modList->getMod(modname); - if(mod.isEnabled()) + if(modList->isModEnabled(modname)) return addError(modname, tr("Mod is already enabled")); if(!mod.isInstalled()) @@ -199,88 +157,32 @@ bool CModManager::canEnableMod(QString modname) if(!mod.isCompatible()) return addError(modname, tr("Mod is not compatible, please update VCMI and checkout latest mod revisions")); - for(auto modEntry : mod.getDependencies()) + if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString()) + return addError(modname, tr("Can not enable translation mod for a different language!")); + + for(const auto & modEntry : mod.getDependencies()) { - if(!modList->hasMod(modEntry)) // required mod is not available + if(!modList->isModExists(modEntry)) // required mod is not available return addError(modname, tr("Required mod %1 is missing").arg(modEntry)); - - CModEntry modData = modList->getMod(modEntry); - - if(!modData.isCompatibilityPatch() && !modData.isEnabled()) - return addError(modname, tr("Required mod %1 is not enabled").arg(modEntry)); } - for(QString modEntry : modList->getModList()) - { - auto mod = modList->getMod(modEntry); - - // "reverse conflict" - enabled mod has this one as conflict - if(mod.isEnabled() && mod.getConflicts().contains(modname)) - return addError(modname, tr("This mod conflicts with %1").arg(modEntry)); - } - - for(auto modEntry : mod.getConflicts()) - { - // check if conflicting mod installed and enabled - if(modList->hasMod(modEntry) && modList->getMod(modEntry).isEnabled()) - return addError(modname, tr("This mod conflicts with %1").arg(modEntry)); - } return true; } -bool CModManager::canDisableMod(QString modname) +bool ModStateController::canDisableMod(QString modname) { auto mod = modList->getMod(modname); - if(mod.isDisabled()) + if(!modList->isModEnabled(modname)) return addError(modname, tr("Mod is already disabled")); if(!mod.isInstalled()) return addError(modname, tr("Mod must be installed first")); - for(QString modEntry : modList->getModList()) - { - auto current = modList->getMod(modEntry); - - if(current.getDependencies().contains(modname) && current.isEnabled()) - return addError(modname, tr("This mod is needed to run %1").arg(modEntry)); - } return true; } -static QVariant writeValue(QString path, QVariantMap input, QVariant value) -{ - if(path.size() > 1) - { - - QString entryName = path.section('/', 0, 1); - QString remainder = "/" + path.section('/', 2, -1); - - entryName.remove(0, 1); - input.insert(entryName, writeValue(remainder, input.value(entryName).toMap(), value)); - return input; - } - else - { - return value; - } -} - -bool CModManager::doEnableMod(QString mod, bool on) -{ - QString path = mod; - path = "/activeMods/" + path.replace(".", "/mods/") + "/active"; - - modSettings = writeValue(path, modSettings, QVariant(on)).toMap(); - modList->setModSettings(modSettings["activeMods"]); - modList->modChanged(mod); - - JsonUtils::JsonToFile(settingsPath(), modSettings); - - return true; -} - -bool CModManager::doInstallMod(QString modname, QString archivePath) +bool ModStateController::doInstallMod(QString modname, QString archivePath) { const auto destDir = CLauncherDirs::modsPath() + QChar{'/'}; @@ -301,7 +203,7 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) { const auto destDirFsPath = qstringToPath(destDir); ZipArchive archive(qstringToPath(archivePath)); - for (auto const & file : filesToExtract) + for(const auto & file : filesToExtract) { if (!archive.extract(destDirFsPath, file)) return false; @@ -333,14 +235,12 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) if(upperLevel != modDirName) removeModDir(destDir + upperLevel); - CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &) { return true; }); - loadMods(); - modList->reloadRepositories(); + modList->reloadLocalState(); return true; } -bool CModManager::doUninstallMod(QString modname) +bool ModStateController::doUninstallMod(QString modname) { ResourcePath resID(std::string("Mods/") + modname.toStdString(), EResType::DIRECTORY); // Get location of the mod, in case-insensitive way @@ -353,14 +253,12 @@ bool CModManager::doUninstallMod(QString modname) if(!removeModDir(modDir)) return addError(modname, tr("Mod is located in protected directory, please remove it manually:\n") + modFullDir.absolutePath()); - CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &){ return true; }); - loadMods(); - modList->reloadRepositories(); + modList->reloadLocalState(); return true; } -bool CModManager::removeModDir(QString path) +bool ModStateController::removeModDir(QString path) { // issues 2673 and 2680 its why you do not recursively remove without sanity check QDir checkDir(path); diff --git a/launcher/modManager/cmodmanager.h b/launcher/modManager/modstatecontroller.h similarity index 69% rename from launcher/modManager/cmodmanager.h rename to launcher/modManager/modstatecontroller.h index 987d1a580..57cfc53e6 100644 --- a/launcher/modManager/cmodmanager.h +++ b/launcher/modManager/modstatecontroller.h @@ -1,5 +1,5 @@ /* - * cmodmanager.h, part of VCMI engine + * modstatecontroller.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,22 +9,24 @@ */ #pragma once -#include "cmodlist.h" +#include -class CModManager : public QObject +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class ModStateModel; + +class ModStateController : public QObject, public boost::noncopyable { Q_OBJECT - CModList * modList; - - QString settingsPath(); + std::shared_ptr modList; // check-free version of public method - bool doEnableMod(QString mod, bool on); bool doInstallMod(QString mod, QString archivePath); bool doUninstallMod(QString mod); - QVariantMap modSettings; QVariantMap localMods; QStringList recentErrors; @@ -32,12 +34,10 @@ class CModManager : public QObject bool removeModDir(QString mod); public: - CModManager(CModList * modList); + ModStateController(std::shared_ptr modList); + ~ModStateController(); - void resetRepositories(); - void loadRepositories(QVector repomap); - void loadModSettings(); - void loadMods(); + void appendRepositories(const JsonNode & repositoriesList); QStringList getErrors(); @@ -46,7 +46,7 @@ public: /// installs mod from zip archive located at archivePath bool installMod(QString mod, QString archivePath); bool uninstallMod(QString mod); - bool enableMod(QString mod); + bool enableMods(QStringList mod); bool disableMod(QString mod); bool canInstallMod(QString mod); diff --git a/launcher/modManager/cmodlistmodel_moc.cpp b/launcher/modManager/modstateitemmodel_moc.cpp similarity index 54% rename from launcher/modManager/cmodlistmodel_moc.cpp rename to launcher/modManager/modstateitemmodel_moc.cpp index 258ffbd83..c81eab38c 100644 --- a/launcher/modManager/cmodlistmodel_moc.cpp +++ b/launcher/modManager/modstateitemmodel_moc.cpp @@ -1,5 +1,5 @@ /* - * cmodlistmodel_moc.cpp, part of VCMI engine + * modstateview_moc.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -8,25 +8,19 @@ * */ #include "StdInc.h" -#include "cmodlistmodel_moc.h" +#include "modstateitemmodel_moc.h" + +#include "modstatemodel.h" #include -namespace ModStatus -{ -static const QString iconDelete = ":/icons/mod-delete.png"; -static const QString iconDisabled = ":/icons/mod-disabled.png"; -static const QString iconDownload = ":/icons/mod-download.png"; -static const QString iconEnabled = ":/icons/mod-enabled.png"; -static const QString iconUpdate = ":/icons/mod-update.png"; -} - -CModListModel::CModListModel(QObject * parent) +ModStateItemModel::ModStateItemModel(std::shared_ptr model, QObject * parent) : QAbstractItemModel(parent) + , model(model) { } -QString CModListModel::modIndexToName(const QModelIndex & index) const +QString ModStateItemModel::modIndexToName(const QModelIndex & index) const { if(index.isValid()) { @@ -36,7 +30,7 @@ QString CModListModel::modIndexToName(const QModelIndex & index) const } -QString CModListModel::modTypeName(QString modTypeID) const +QString ModStateItemModel::modTypeName(QString modTypeID) const { static const QMap modTypes = { {"Translation", tr("Translation")}, @@ -69,28 +63,28 @@ QString CModListModel::modTypeName(QString modTypeID) const return tr("Other"); } -QVariant CModListModel::getValue(const CModEntry & mod, int field) const +QVariant ModStateItemModel::getValue(const ModState & mod, int field) const { switch(field) { case ModFields::STATUS_ENABLED: - return mod.getModStatus() & (ModStatus::ENABLED | ModStatus::INSTALLED); + return model->isModEnabled(mod.getID()); case ModFields::STATUS_UPDATE: - return mod.getModStatus() & (ModStatus::UPDATEABLE | ModStatus::INSTALLED); + return model->isModUpdateAvailable(mod.getID()); case ModFields::NAME: - return mod.getValue("name"); + return mod.getName(); case ModFields::TYPE: - return modTypeName(mod.getValue("modType").toString()); + return modTypeName(mod.getType()); default: return QVariant(); } } -QVariant CModListModel::getText(const CModEntry & mod, int field) const +QVariant ModStateItemModel::getText(const ModState & mod, int field) const { switch(field) { @@ -102,31 +96,58 @@ QVariant CModListModel::getText(const CModEntry & mod, int field) const } } -QVariant CModListModel::getIcon(const CModEntry & mod, int field) const +QVariant ModStateItemModel::getIcon(const ModState & mod, int field) const { - if(field == ModFields::STATUS_ENABLED && mod.isEnabled()) - return QIcon(ModStatus::iconEnabled); - if(field == ModFields::STATUS_ENABLED && mod.isDisabled()) - return QIcon(ModStatus::iconDisabled); + static const QString iconDisabled = ":/icons/mod-disabled.png"; + static const QString iconDisabledSubmod = ":/icons/submod-disabled.png"; + static const QString iconDownload = ":/icons/mod-download.png"; + static const QString iconEnabled = ":/icons/mod-enabled.png"; + static const QString iconEnabledSubmod = ":/icons/submod-enabled.png"; + static const QString iconUpdate = ":/icons/mod-update.png"; - if(field == ModFields::STATUS_UPDATE && mod.isUpdateable()) - return QIcon(ModStatus::iconUpdate); - if(field == ModFields::STATUS_UPDATE && !mod.isInstalled()) - return QIcon(ModStatus::iconDownload); + if (field == ModFields::STATUS_ENABLED) + { + if (!model->isModInstalled(mod.getID())) + return QVariant(); + + if(mod.isSubmod() && !model->isModEnabled(mod.getTopParentID())) + { + QString topParentID = mod.getTopParentID(); + QString settingID = mod.getID().section('.', 1); + + if (model->isModSettingEnabled(topParentID, settingID)) + return QIcon(iconEnabledSubmod); + else + return QIcon(iconDisabledSubmod); + } + + if (model->isModEnabled(mod.getID())) + return QIcon(iconEnabled); + else + return QIcon(iconDisabled); + } + + if(field == ModFields::STATUS_UPDATE) + { + if (model->isModUpdateAvailable(mod.getID())) + return QIcon(iconUpdate); + if (!model->isModInstalled(mod.getID())) + return QIcon(iconDownload); + } return QVariant(); } -QVariant CModListModel::getTextAlign(int field) const +QVariant ModStateItemModel::getTextAlign(int field) const { return QVariant(Qt::AlignLeft | Qt::AlignVCenter); } -QVariant CModListModel::data(const QModelIndex & index, int role) const +QVariant ModStateItemModel::data(const QModelIndex & index, int role) const { if(index.isValid()) { - auto mod = getMod(modIndexToName(index)); + auto mod = model->getMod(modIndexToName(index)); switch(role) { @@ -139,32 +160,32 @@ QVariant CModListModel::data(const QModelIndex & index, int role) const case ModRoles::ValueRole: return getValue(mod, index.column()); case ModRoles::ModNameRole: - return mod.getName(); + return mod.getID(); } } return QVariant(); } -int CModListModel::rowCount(const QModelIndex & index) const +int ModStateItemModel::rowCount(const QModelIndex & index) const { if(index.isValid()) return modIndex[modIndexToName(index)].size(); return modIndex[""].size(); } -int CModListModel::columnCount(const QModelIndex &) const +int ModStateItemModel::columnCount(const QModelIndex &) const { return ModFields::COUNT; } -Qt::ItemFlags CModListModel::flags(const QModelIndex &) const +Qt::ItemFlags ModStateItemModel::flags(const QModelIndex &) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } -QVariant CModListModel::headerData(int section, Qt::Orientation orientation, int role) const +QVariant ModStateItemModel::headerData(int section, Qt::Orientation orientation, int role) const { - static const QString header[ModFields::COUNT] = + static const std::array header = { QT_TRANSLATE_NOOP("ModFields", "Name"), QT_TRANSLATE_NOOP("ModFields", ""), // status icon @@ -173,24 +194,17 @@ QVariant CModListModel::headerData(int section, Qt::Orientation orientation, int }; if(role == Qt::DisplayRole && orientation == Qt::Horizontal) - return QCoreApplication::translate("ModFields", header[section].toStdString().c_str()); + return QCoreApplication::translate("ModFields", header[section]); return QVariant(); } -void CModListModel::reloadRepositories() +void ModStateItemModel::reloadRepositories() { beginResetModel(); endResetModel(); } -void CModListModel::resetRepositories() -{ - beginResetModel(); - CModList::resetRepositories(); - endResetModel(); -} - -void CModListModel::modChanged(QString modID) +void ModStateItemModel::modChanged(QString modID) { int index = modNameToID.indexOf(modID); QModelIndex parent = this->parent(createIndex(0, 0, index)); @@ -198,9 +212,9 @@ void CModListModel::modChanged(QString modID) emit dataChanged(createIndex(row, 0, index), createIndex(row, 4, index)); } -void CModListModel::endResetModel() +void ModStateItemModel::endResetModel() { - modNameToID = getModList(); + modNameToID = model->getAllMods(); modIndex.clear(); for(const QString & str : modNameToID) { @@ -216,7 +230,7 @@ void CModListModel::endResetModel() QAbstractItemModel::endResetModel(); } -QModelIndex CModListModel::index(int row, int column, const QModelIndex & parent) const +QModelIndex ModStateItemModel::index(int row, int column, const QModelIndex & parent) const { if(parent.isValid()) { @@ -231,7 +245,7 @@ QModelIndex CModListModel::index(int row, int column, const QModelIndex & parent return QModelIndex(); } -QModelIndex CModListModel::parent(const QModelIndex & child) const +QModelIndex ModStateItemModel::parent(const QModelIndex & child) const { QString modID = modNameToID[child.internalId()]; for(auto entry = modIndex.begin(); entry != modIndex.end(); entry++) // because using range-for entry type is QMap::value_type oO @@ -244,26 +258,46 @@ QModelIndex CModListModel::parent(const QModelIndex & child) const return QModelIndex(); } -void CModFilterModel::setTypeFilter(int filteredType, int filterMask) +void CModFilterModel::setTypeFilter(ModFilterMask newFilterMask) { - this->filterMask = filterMask; - this->filteredType = filteredType; + filterMask = newFilterMask; invalidateFilter(); } +bool CModFilterModel::filterMatchesCategory(const QModelIndex & source) const +{ + QString modID =source.data(ModRoles::ModNameRole).toString(); + ModState mod = base->model->getMod(modID); + + switch (filterMask) + { + case ModFilterMask::ALL: + return true; + case ModFilterMask::AVAILABLE: + return !mod.isInstalled(); + case ModFilterMask::INSTALLED: + return mod.isInstalled(); + case ModFilterMask::UPDATEABLE: + return mod.isUpdateAvailable(); + case ModFilterMask::ENABLED: + return mod.isInstalled() && base->model->isModEnabled(modID); + case ModFilterMask::DISABLED: + return mod.isInstalled() && !base->model->isModEnabled(modID); + } + assert(0); + return false; +} + bool CModFilterModel::filterMatchesThis(const QModelIndex & source) const { - CModEntry mod = base->getMod(source.data(ModRoles::ModNameRole).toString()); - return (mod.getModStatus() & filterMask) == filteredType && - QSortFilterProxyModel::filterAcceptsRow(source.row(), source.parent()); + return filterMatchesCategory(source) && QSortFilterProxyModel::filterAcceptsRow(source.row(), source.parent()); } bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex & source_parent) const { QModelIndex index = base->index(source_row, 0, source_parent); - - CModEntry mod = base->getMod(index.data(ModRoles::ModNameRole).toString()); - if (!mod.isVisible()) + QString modID = index.data(ModRoles::ModNameRole).toString(); + if (base->model->getMod(modID).isHidden()) return false; if(filterMatchesThis(index)) @@ -273,7 +307,7 @@ bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex & sourc for(size_t i = 0; i < base->rowCount(index); i++) { - if(filterMatchesThis(base->index((int)i, 0, index))) + if(filterMatchesThis(base->index(i, 0, index))) return true; } @@ -287,8 +321,8 @@ bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex & sourc return false; } -CModFilterModel::CModFilterModel(CModListModel * model, QObject * parent) - : QSortFilterProxyModel(parent), base(model), filteredType(ModStatus::MASK_NONE), filterMask(ModStatus::MASK_NONE) +CModFilterModel::CModFilterModel(ModStateItemModel * model, QObject * parent) + : QSortFilterProxyModel(parent), base(model), filterMask(ModFilterMask::ALL) { setSourceModel(model); setSortRole(ModRoles::ValueRole); diff --git a/launcher/modManager/cmodlistmodel_moc.h b/launcher/modManager/modstateitemmodel_moc.h similarity index 58% rename from launcher/modManager/cmodlistmodel_moc.h rename to launcher/modManager/modstateitemmodel_moc.h index 783322029..22f1da0f4 100644 --- a/launcher/modManager/cmodlistmodel_moc.h +++ b/launcher/modManager/modstateitemmodel_moc.h @@ -1,5 +1,5 @@ /* - * cmodlistmodel_moc.h, part of VCMI engine + * modstateview_moc.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,11 +9,12 @@ */ #pragma once -#include "cmodlist.h" - #include #include +class ModStateModel; +class ModState; + namespace ModFields { enum EModFields @@ -26,6 +27,16 @@ enum EModFields }; } +enum class ModFilterMask : uint8_t +{ + ALL, + AVAILABLE, + INSTALLED, + UPDATEABLE, + ENABLED, + DISABLED +}; + namespace ModRoles { enum EModRoles @@ -35,14 +46,17 @@ enum EModRoles }; } -class CModListModel : public QAbstractItemModel, public CModList +class ModStateItemModel final : public QAbstractItemModel { + friend class CModFilterModel; Q_OBJECT - QVector modNameToID; + std::shared_ptr model; + + QStringList modNameToID; // contains mapping mod -> numbered list of submods // mods that have no parent located under "" key (empty string) - QMap> modIndex; + QMap modIndex; void endResetModel(); @@ -50,17 +64,16 @@ class CModListModel : public QAbstractItemModel, public CModList QString modTypeName(QString modTypeID) const; QVariant getTextAlign(int field) const; - QVariant getValue(const CModEntry & mod, int field) const; - QVariant getText(const CModEntry & mod, int field) const; - QVariant getIcon(const CModEntry & mod, int field) const; + QVariant getValue(const ModState & mod, int field) const; + QVariant getText(const ModState & mod, int field) const; + QVariant getIcon(const ModState & mod, int field) const; public: - explicit CModListModel(QObject * parent = nullptr); + explicit ModStateItemModel(std::shared_ptr model, QObject * parent); /// CModListContainer overrides - void resetRepositories() override; - void reloadRepositories() override; - void modChanged(QString modID) override; + void reloadRepositories(); + void modChanged(QString modID); QVariant data(const QModelIndex & index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; @@ -68,29 +81,24 @@ public: int rowCount(const QModelIndex & parent) const override; int columnCount(const QModelIndex & parent) const override; - QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const override; + QModelIndex index(int row, int column, const QModelIndex & parent) const override; QModelIndex parent(const QModelIndex & child) const override; Qt::ItemFlags flags(const QModelIndex & index) const override; - -signals: - -public slots: - }; -class CModFilterModel : public QSortFilterProxyModel +class CModFilterModel final : public QSortFilterProxyModel { - CModListModel * base; - int filteredType; - int filterMask; + ModStateItemModel * base; + ModFilterMask filterMask; bool filterMatchesThis(const QModelIndex & source) const; + bool filterMatchesCategory(const QModelIndex & source) const; bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override; public: - void setTypeFilter(int filteredType, int filterMask); + void setTypeFilter(ModFilterMask filterMask); - CModFilterModel(CModListModel * model, QObject * parent = nullptr); + CModFilterModel(ModStateItemModel * model, QObject * parent = nullptr); }; diff --git a/launcher/modManager/modstatemodel.cpp b/launcher/modManager/modstatemodel.cpp new file mode 100644 index 000000000..be4bc5627 --- /dev/null +++ b/launcher/modManager/modstatemodel.cpp @@ -0,0 +1,130 @@ +/* + * modstatemodel.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "modstatemodel.h" + +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/json/JsonUtils.h" +#include "../../lib/modding/ModManager.h" + +ModStateModel::ModStateModel() + : repositoryData(std::make_unique()) + , modManager(std::make_unique()) +{ +} + +ModStateModel::~ModStateModel() = default; + +void ModStateModel::appendRepositories(const JsonNode & repositoriesList) +{ + JsonUtils::mergeCopy(*repositoryData, repositoriesList); + + modManager = std::make_unique(*repositoryData); +} + +void ModStateModel::reloadLocalState() +{ + CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &){ return true; }); + modManager = std::make_unique(*repositoryData); +} + +const JsonNode & ModStateModel::getRepositoryData() const +{ + return *repositoryData; +} + +ModState ModStateModel::getMod(QString modName) const +{ + assert(modName.toLower() == modName); + return ModState(modManager->getModDescription(modName.toStdString())); +} + +template +QStringList stringListStdToQt(const Container & container) +{ + QStringList result; + for (const auto & str : container) + result.push_back(QString::fromStdString(str)); + return result; +} + +QStringList ModStateModel::getAllMods() const +{ + return stringListStdToQt(modManager->getAllMods()); +} + +bool ModStateModel::isModExists(QString modName) const +{ + return vstd::contains(modManager->getAllMods(), modName.toStdString()); +} + +bool ModStateModel::isModInstalled(QString modName) const +{ + return getMod(modName).isInstalled(); +} + +bool ModStateModel::isModSettingEnabled(QString rootModName, QString modSettingName) const +{ + return modManager->isModSettingActive(rootModName.toStdString(), modSettingName.toStdString()); +} + +bool ModStateModel::isModEnabled(QString modName) const +{ + return modManager->isModActive(modName.toStdString()); +} + +bool ModStateModel::isModUpdateAvailable(QString modName) const +{ + return getMod(modName).isUpdateAvailable(); +} + +bool ModStateModel::isModVisible(QString modName) const +{ + return getMod(modName).isVisible(); +} + +QString ModStateModel::getInstalledModSizeFormatted(QString modName) const +{ + return QCoreApplication::translate("File size", "%1 MiB").arg(QString::number(getInstalledModSizeMegabytes(modName), 'f', 1)); +} + +double ModStateModel::getInstalledModSizeMegabytes(QString modName) const +{ + return modManager->getInstalledModSizeMegabytes(modName.toStdString()); +} + +void ModStateModel::doEnableMods(QStringList modList) +{ + std::vector stdList; + + for (const auto & entry : modList) + stdList.push_back(entry.toStdString()); + + modManager->tryEnableMods(stdList); +} + +void ModStateModel::doDisableMod(QString modname) +{ + modManager->tryDisableMod(modname.toStdString()); +} + +bool ModStateModel::isSubmod(QString modname) +{ + return modname.contains('.'); +} + +QString ModStateModel::getTopParent(QString modname) const +{ + QStringList components = modname.split('.'); + if (components.size() > 1) + return components.front(); + else + return ""; +} diff --git a/launcher/modManager/modstatemodel.h b/launcher/modManager/modstatemodel.h new file mode 100644 index 000000000..08bf1935f --- /dev/null +++ b/launcher/modManager/modstatemodel.h @@ -0,0 +1,52 @@ +/* + * modstatemodel.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "modstate.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +class ModManager; +VCMI_LIB_NAMESPACE_END + +/// Class that represent current state of available mods +/// Provides Qt-based interface to library class ModManager +class ModStateModel +{ + std::unique_ptr repositoryData; + std::unique_ptr modManager; + +public: + ModStateModel(); + ~ModStateModel(); + + void appendRepositories(const JsonNode & repositoriesList); + void reloadLocalState(); + const JsonNode & getRepositoryData() const; + + ModState getMod(QString modName) const; + QStringList getAllMods() const; + + QString getInstalledModSizeFormatted(QString modName) const; + double getInstalledModSizeMegabytes(QString modName) const; + + bool isModExists(QString modName) const; + bool isModInstalled(QString modName) const; + bool isModEnabled(QString modName) const; + bool isModSettingEnabled(QString rootModName, QString modSettingName) const; + bool isModUpdateAvailable(QString modName) const; + bool isModVisible(QString modName) const; + + void doEnableMods(QStringList modList); + void doDisableMod(QString modname); + + bool isSubmod(QString modname); + QString getTopParent(QString modname) const; +}; diff --git a/launcher/resources.qrc b/launcher/resources.qrc index 139401504..5be9d4f1b 100644 --- a/launcher/resources.qrc +++ b/launcher/resources.qrc @@ -7,8 +7,10 @@ icons/menu-settings.png icons/mod-delete.png icons/mod-disabled.png + icons/submod-disabled.png icons/mod-download.png icons/mod-enabled.png + icons/submod-enabled.png icons/mod-update.png diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index fe5e964e6..dc77e03a3 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -90,109 +90,85 @@ CModListModel - Translation - 本地化 + 本地化 - Town - 城镇 + 城镇 - Test - 测试 + 测试 - Templates - 地图模板 + 地图模板 - Spells - 法术 + 法术 - Music - 音乐 + 音乐 - Maps - 地图 + 地图 - Sounds - 音效 + 音效 - Skills - 技能 + 技能 - - Other - 其他 + 其他 - Objects - 物件 + 物件 - - Mechanics 无法确定是否分类是游戏机制或者是游戏中的战争器械 - 机制 + 机制 - - Interface - 界面 + 界面 - Heroes - 英雄 + 英雄 - - Graphical - 图像 + 图像 - Expansion - 扩展包 + 扩展包 - Creatures - 生物 + 生物 - Compatibility - 兼容性 + 兼容性 - Artifacts - 宝物 + 宝物 - AI - AI + AI @@ -235,7 +211,7 @@ - + Description 详细介绍 @@ -295,191 +271,204 @@ 终止 - + Mod name 模组名称 - + + Installed version 已安装的版本 - + + Latest version 最新版本 - + Size 大小 - + Download size 下载大小 - + Authors 作者 - + License 授权许可 - + Contact 联系方式 - + Compatibility 兼容性 - - + + Required VCMI version 需要VCMI版本 - + Supported VCMI version 支持的VCMI版本 - + please upgrade mod 请更新模组 - - + + mods repository index 模组源索引号 - + or newer 或更新的版本 - + Supported VCMI versions 支持的VCMI版本 - + Languages 语言 - + Required mods Mod统一翻译为模组 前置模组 - + Conflicting mods Mod统一翻译为模组 冲突的模组 - This mod can not be installed or enabled because the following dependencies are not present - 这个模组无法被安装或者激活,因为下列依赖项未满足 + 这个模组无法被安装或者激活,因为下列依赖项未满足 - This mod can not be enabled because the following mods are incompatible with it - 这个模组无法被激活,因为下列模组与其不兼容 + 这个模组无法被激活,因为下列模组与其不兼容 - This mod cannot be disabled because it is required by the following mods - 这个模组无法被禁用,因为它被下列模组所依赖 + 这个模组无法被禁用,因为它被下列模组所依赖 - This mod cannot be uninstalled or updated because it is required by the following mods - 这个模组无法被卸载或者更新,因为它被下列模组所依赖 + 这个模组无法被卸载或者更新,因为它被下列模组所依赖 - + + This mod cannot be enabled because it translates into a different language. + 这个模组无法被启用,因为它被翻译成其他语言。 + + + + This mod can not be enabled because the following dependencies are not present + 这个模组无法被启用,因为下列依赖不满足 + + + + This mod can not be installed because the following dependencies are not present + 这个模组无法被安装,因为下列依赖不满足 + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod 这是一个附属模组它无法在所属模组外被直接被安装或者卸载 - + Notes 笔记注释 - + All supported files 所有支持的文件格式 - + Maps 地图 - + Campaigns 战役 - + Configs 配置 - + Mods 模组 - + Gog files Gog文件 - + All files (*.*) 所有文件 (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... 选择需要安装的文件(配置,模组,地图,战役,gog文件)... - + Replace config file? 替换配置文件? - + Do you want to replace %1? 你想要替换%1吗? - + Downloading %1. %p% (%v MB out of %m MB) finished 正在下载 %1. %p% (%v MB 共 %m MB) 已完成 - + Download failed 下载失败 - + Unable to download all files. Encountered errors: @@ -492,7 +481,7 @@ Encountered errors: - + Install successfully downloaded? @@ -501,39 +490,39 @@ Install successfully downloaded? 安装下载成功的部分? - + Installing chronicles 安装历代记 - + Installing mod %1 正在安装模组 %1 - + Operation failed 操作失败 - + Encountered errors: 遇到问题: - + screenshots 截图 - + Screenshot %1 截图 %1 - + Mod is incompatible Mod统一翻译为模组 模组不兼容 @@ -542,97 +531,77 @@ Install successfully downloaded? CModManager - Can not install submod - 无法安装子模组 + 无法安装子模组 - Mod is already installed - 模组已安装 + 模组已安装 - Can not uninstall submod - 无法卸载子模组 + 无法卸载子模组 - Mod is not installed - 模组未安装 + 模组未安装 - Mod is already enabled - 模组已启用 + 模组已启用 - - Mod must be installed first - 需要先安装模组 + 需要先安装模组 - Mod is not compatible, please update VCMI and checkout latest mod revisions - 模组不兼容,请更新VCMI并获取模组最新版本 + 模组不兼容,请更新VCMI并获取模组最新版本 - Required mod %1 is missing - 需要的模组%1未找到 + 需要的模组%1未找到 - Required mod %1 is not enabled - 需要的模组%1未启用 + 需要的模组%1未启用 - - This mod conflicts with %1 - 此模组和%1冲突 + 此模组和%1冲突 - Mod is already disabled - 模组已禁用 + 模组已禁用 - This mod is needed to run %1 - 此模组需要运行%1 + 此模组需要运行%1 - Mod archive is missing - 模组归档文件未找到 + 模组归档文件未找到 - Mod with such name is already installed - 同名模组已安装 + 同名模组已安装 - Mod archive is invalid or corrupted - 模组归档文件无效或损坏 + 模组归档文件无效或损坏 - Failed to extract mod data - 提取模组数据失败 + 提取模组数据失败 - Data with this mod was not found - 此模组的数据未找到 + 此模组的数据未找到 - Mod is located in protected directory, please remove it manually: - 模组位于受保护的目录,请手动删除它: + 模组位于受保护的目录,请手动删除它: @@ -1099,29 +1068,26 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1363,7 +1329,7 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b GOG data - GOC数据 + GOG数据 @@ -1572,16 +1538,209 @@ error reason: ModFields - + Name 名称 - + Type 类型 + + ModStateController + + + Can not install submod + 无法安装子模组 + + + + Mod is already installed + 模组已安装 + + + + Can not uninstall submod + 无法卸载子模组 + + + + Mod is not installed + 模组未安装 + + + + Mod is already enabled + 模组已启用 + + + + + Mod must be installed first + 需要先安装模组 + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + 模组不兼容,请更新VCMI并获取模组最新版本 + + + + Can not enable translation mod for a different language! + 无法启用不同语言的模组! + + + + Required mod %1 is missing + 需要的模组%1未找到 + + + + Mod is already disabled + 模组已禁用 + + + + Mod archive is missing + 模组归档文件未找到 + + + + Mod with such name is already installed + 同名模组已安装 + + + + Mod archive is invalid or corrupted + 模组归档文件无效或损坏 + + + + Failed to extract mod data + 提取模组数据失败 + + + + Data with this mod was not found + 此模组的数据未找到 + + + + Mod is located in protected directory, please remove it manually: + + 模组位于受保护的目录,请手动删除它: + + + + + ModStateItemModel + + + Translation + 本地化 + + + + Town + 城镇 + + + + Test + 测试 + + + + Templates + 地图模板 + + + + Spells + 法术 + + + + Music + 音乐 + + + + Maps + 地图 + + + + Sounds + 音效 + + + + Skills + 技能 + + + + + Other + 其他 + + + + Objects + 物件 + + + + + Mechanics + 机制 + + + + + Interface + 界面 + + + + Heroes + 英雄 + + + + + Graphical + 图像 + + + + Expansion + 扩展包 + + + + Creatures + 生物 + + + + Compatibility + 兼容性 + + + + Artifacts + 宝物 + + + + AI + AI + + QObject diff --git a/launcher/translation/czech.ts b/launcher/translation/czech.ts index 3668ded50..b42d6ee60 100644 --- a/launcher/translation/czech.ts +++ b/launcher/translation/czech.ts @@ -87,113 +87,6 @@ Nahlásit chybu - - CModListModel - - - Translation - Překlad - - - - Town - Město - - - - Test - Zkouška - - - - Templates - Šablony - - - - Spells - Kouzla - - - - Music - Hudba - - - - Maps - Mapy - - - - Sounds - Zvuky - - - - Skills - Schopnosti - - - - - Other - Ostatní - - - - Objects - Objekty - - - - - Mechanics - Mechaniky - - - - - Interface - Rozhraní - - - - Heroes - Hrdinové - - - - - Graphical - Grafika - - - - Expansion - Rozšíření - - - - Creatures - Jednotky - - - - Compatibility - Kompabilita - - - - Artifacts - Artefakty - - - - AI - AI - - CModListView @@ -233,7 +126,7 @@ - + Description Popis @@ -293,189 +186,186 @@ Zrušit - + Mod name Název modifikace - + + Installed version Nainstalovaná verze - + + Latest version Nejnovější verze - + Size Velikost - + Download size Velikost ke stažení - + Authors Autoři - + License Licence - + Contact Kontakt - + Compatibility Kompabilita - - + + Required VCMI version Vyžadovaná verze VCMI - + Supported VCMI version Podporovaná verze VCMI - + please upgrade mod prosíme aktualizujte modifikaci - - + + mods repository index index repozitáře modifikací - + or newer nebo novější - + Supported VCMI versions Podporované verze VCMI - + Languages Jazyky - + Required mods Vyžadované modifikace VCMI - + Conflicting mods Modifikace v kolizi - - This mod can not be installed or enabled because the following dependencies are not present - Tato modifikace nelze nainstalovat ani povolit, protože nejsou přítomny následující závislosti + + This mod cannot be enabled because it translates into a different language. + Tuto modifikaci nelze aktivovat, protože je určena pro jiný jazyk. - - This mod can not be enabled because the following mods are incompatible with it - Tato modifikace nemůže být povolena, protože není kompatibilní s následujícími modifikacemi + + This mod can not be enabled because the following dependencies are not present + Tuto modifikaci nelze aktivovat, protože chybí následující závislosti - - This mod cannot be disabled because it is required by the following mods - Tato modifikace nemůže být zakázána, protože je vyžadována následujícími modifikacemi + + This mod can not be installed because the following dependencies are not present + Tuto modifikaci nelze nainstalovat, protože chybí následující závislosti - - This mod cannot be uninstalled or updated because it is required by the following mods - Tato modifikace nemůže být odinstalována nebo aktualizována, protože je vyžadována následujícími modifikacemi - - - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Toto je podmodifikace a nelze ji nainstalovat ani odinstalovat samostatně bez hlavní modifikace - + Notes Poznámky - + All supported files Všechny podporované soubory - + Maps Mapy - + Campaigns Kampaně - + Configs Nastavení - + Mods Modifikace - + Gog files Soubory GOG - + All files (*.*) Všechny soubory (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... Vyberte soubory (konfigurace, modifikace, mapy, kampaně, soubory GOG) k instalaci... - + Replace config file? Nahradit soubor nastavení? - + Do you want to replace %1? Chcete nahradit %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Stahování %1. %p% (%v MB z %m MB) dokončeno - + Download failed Stahování selhalo - + Unable to download all files. Encountered errors: @@ -488,7 +378,7 @@ Vyskytly se chyby: - + Install successfully downloaded? @@ -497,140 +387,43 @@ Install successfully downloaded? Nainstalovat úspěšně stažené? - + Installing chronicles Instalování kronik - + Installing mod %1 Instalování modifikace %1 - + Operation failed Operace selhala - + Encountered errors: Vyskytly se chyby: - + screenshots snímky obrazovky - + Screenshot %1 Snímek obrazovky %1 - + Mod is incompatible Modifikace není kompatibilní - - CModManager - - - Can not install submod - Nelze nainstalovat podmodifikaci - - - - Mod is already installed - Modifikace je již nainstalována - - - - Can not uninstall submod - Nelze odinstalovat podmodifikaci - - - - Mod is not installed - Modifikace není nainstalována - - - - Mod is already enabled - Modifikace je již povolena - - - - - Mod must be installed first - Nejprve musí být nainstalována modifikace - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - Modifikace není kompatibilní, prosíme aktualizujte VCMI a použijte nejnovější verzi modifikace - - - - Required mod %1 is missing - Vyžadovaná modifkace %1 chybí - - - - Required mod %1 is not enabled - Vyžadovaná modifikace %1 není povolena - - - - - This mod conflicts with %1 - Tato modifikace koliduje s %1 - - - - Mod is already disabled - Modifikace je již povolena - - - - This mod is needed to run %1 - Modifikace %1 je vyžadována pro běh - - - - Mod archive is missing - Archiv modifikace chybí - - - - Mod with such name is already installed - Modifikace s tímto názvem je již nainstalována - - - - Mod archive is invalid or corrupted - Archiv modifikace je neplatný nebo poškozený - - - - Failed to extract mod data - Extrakce dat modifikace selhala - - - - Data with this mod was not found - Data s touto modifikací nebyla nalezena - - - - Mod is located in protected directory, please remove it manually: - - Modifikace se nachází v zabezpečené složce, prosíme odstraňte ji ručně: - - - CSettingsView @@ -664,11 +457,6 @@ Nainstalovat úspěšně stažené? Additional repository Další repozitáře - - - Downscaling Filter - Filtr pro zmenšování - Adventure Map Allies @@ -901,6 +689,11 @@ Nainstalovat úspěšně stažené? Autosave limit (0 = off) Limit aut. uložení (0=vypnuto) + + + Downscaling Filter + Filtr pro zmenšování + Framerate Limit @@ -1093,30 +886,11 @@ Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybra File size - - %1 B - %1 B - - - - %1 KiB - %1 KiB - - - + + %1 MiB %1 MiB - - - %1 GiB - %1 GiB - - - - %1 TiB - %1 TiB - FirstLaunchView @@ -1160,7 +934,7 @@ Než začnete hrát, je třeba dokončit několik kroků. Pamatujte, že pro používání VCMI musíte vlastnit originální herní soubory pro Heroes® of Might and Magic® III: Complete nebo The Shadow of Death. -Heroes® of Might and Magic® III HD momentálně není podporováno! +Heroes® of Might and Magic® III HD momentálně není podporována! @@ -1289,7 +1063,7 @@ Offline instalátor obsahuje dvě části, .exe a .bin. Ujistěte se, že stahuj Install VCMI Mod Preset - Instalovat předvybrané modifiakce VCMI + Instalovat předvybrané VCMI modifikace @@ -1565,16 +1339,209 @@ Důvod chyby: ModFields - + Name Název - + Type Druh + + ModStateController + + + Can not install submod + Nelze nainstalovat podmodifikaci + + + + Mod is already installed + Modifikace je již nainstalována + + + + Can not uninstall submod + Nelze odinstalovat podmodifikaci + + + + Mod is not installed + Modifikace není nainstalována + + + + Mod is already enabled + Modifikace je již povolena + + + + + Mod must be installed first + Nejprve je třeba nainstalovat modifikaci + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Modifikace není kompatibilní, prosím aktualizujte VCMI a použijte nejnovější verzi modifikace + + + + Can not enable translation mod for a different language! + Nelze zapnout modifikaci s překladem pro jiný jazyk! + + + + Required mod %1 is missing + Vyžadovaná modifkace %1 chybí + + + + Mod is already disabled + Modifikace je již deaktivována + + + + Mod archive is missing + Archiv modifikace chybí + + + + Mod with such name is already installed + Modifikace s tímto názvem je již nainstalována + + + + Mod archive is invalid or corrupted + Archiv modifikace je neplatný nebo poškozený + + + + Failed to extract mod data + Extrakce modifikace selhala + + + + Data with this mod was not found + Data s touto modifikací nebyla nalezena + + + + Mod is located in protected directory, please remove it manually: + + Modifikace se nachází v zabezpečené složce, prosím odstraňte ji ručně: + + + + + ModStateItemModel + + + Translation + Překlad + + + + Town + Město + + + + Test + Zkouška + + + + Templates + Šablony + + + + Spells + Kouzla + + + + Music + Hudba + + + + Maps + Mapy + + + + Sounds + Zvuky + + + + Skills + Schopnosti + + + + + Other + Ostatní + + + + Objects + Objekty + + + + + Mechanics + Mechaniky + + + + + Interface + Rozhraní + + + + Heroes + Hrdinové + + + + + Graphical + Grafika + + + + Expansion + Rozšíření + + + + Creatures + Jednotky + + + + Compatibility + Kompatibilita + + + + Artifacts + Artefakty + + + + AI + AI + + QObject diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index 63a07403e..5c186eaa0 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -87,113 +87,6 @@ - - CModListModel - - - Translation - - - - - Town - - - - - Test - - - - - Templates - - - - - Spells - - - - - Music - - - - - Maps - - - - - Sounds - - - - - Skills - - - - - - Other - - - - - Objects - - - - - - Mechanics - - - - - - Interface - - - - - Heroes - - - - - - Graphical - - - - - Expansion - - - - - Creatures - - - - - Compatibility - - - - - Artifacts - - - - - AI - - - CModListView @@ -233,7 +126,7 @@ - + Description @@ -293,189 +186,186 @@ - + Mod name - + + Installed version - + + Latest version - + Size - + Download size - + Authors - + License - + Contact - + Compatibility - - + + Required VCMI version - + Supported VCMI version - + please upgrade mod - - + + mods repository index - + or newer - + Supported VCMI versions - + Languages - + Required mods - + Conflicting mods - - This mod can not be installed or enabled because the following dependencies are not present + + This mod cannot be enabled because it translates into a different language. - - This mod can not be enabled because the following mods are incompatible with it + + This mod can not be enabled because the following dependencies are not present - - This mod cannot be disabled because it is required by the following mods + + This mod can not be installed because the following dependencies are not present - - This mod cannot be uninstalled or updated because it is required by the following mods - - - - + This is a submod and it cannot be installed or uninstalled separately from its parent mod - + Notes - + All supported files - + Maps - + Campaigns - + Configs - + Mods - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? - + Do you want to replace %1? - + Downloading %1. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -484,145 +374,49 @@ Encountered errors: - + Install successfully downloaded? - + Installing chronicles - + Installing mod %1 - + Operation failed - + Encountered errors: - + screenshots - + Screenshot %1 - + Mod is incompatible - - CModManager - - - Can not install submod - - - - - Mod is already installed - - - - - Can not uninstall submod - - - - - Mod is not installed - - - - - Mod is already enabled - - - - - - Mod must be installed first - - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - - - - - Required mod %1 is missing - - - - - Required mod %1 is not enabled - - - - - - This mod conflicts with %1 - - - - - Mod is already disabled - - - - - This mod is needed to run %1 - - - - - Mod archive is missing - - - - - Mod with such name is already installed - - - - - Mod archive is invalid or corrupted - - - - - Failed to extract mod data - - - - - Data with this mod was not found - - - - - Mod is located in protected directory, please remove it manually: - - - - CSettingsView @@ -1079,30 +873,11 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1540,16 +1315,208 @@ error reason: ModFields - + Name - + Type + + ModStateController + + + Can not install submod + + + + + Mod is already installed + + + + + Can not uninstall submod + + + + + Mod is not installed + + + + + Mod is already enabled + + + + + + Mod must be installed first + + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + + + + + Mod is already disabled + + + + + Mod archive is missing + + + + + Mod with such name is already installed + + + + + Mod archive is invalid or corrupted + + + + + Failed to extract mod data + + + + + Data with this mod was not found + + + + + Mod is located in protected directory, please remove it manually: + + + + + + ModStateItemModel + + + Translation + + + + + Town + + + + + Test + + + + + Templates + + + + + Spells + + + + + Music + + + + + Maps + + + + + Sounds + + + + + Skills + + + + + + Other + + + + + Objects + + + + + + Mechanics + + + + + + Interface + + + + + Heroes + + + + + + Graphical + + + + + Expansion + + + + + Creatures + + + + + Compatibility + + + + + Artifacts + + + + + AI + + + QObject diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index c20d1f79a..3c6a3f946 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Traduction + Traduction - Town - Ville + Ville - Test - Test + Test - Templates - Modèles + Modèles - Spells - Sorts + Sorts - Music - Musique + Musique - Maps - Cartes + Cartes - Sounds - Sons + Sons - Skills - Compétences + Compétences - - Other - Autre + Autre - Objects - Objets + Objets - - Mechanics - Mécaniques + Mécaniques - - Interface - Interface + Interface - Heroes - Héros + Héros - - Graphical - Graphisme + Graphisme - Expansion - Extension + Extension - Creatures - Créatures + Créatures - Compatibility - Compatibilité + Compatibilité - Artifacts - Artefacts + Artefacts - AI - IA + IA @@ -238,7 +214,7 @@ - + Description Description @@ -293,194 +269,207 @@ Abandonner - + Mod name Nom du mod - + + Installed version Version installée - + + Latest version Dernière version - + Size Taille - + Download size Taille de téléchargement - + Authors Auteur(s) - + License Licence - + Contact Contact - + Compatibility Compatibilité - - + + Required VCMI version Version requise de VCMI - + Supported VCMI version Version supportée de VCMI - + please upgrade mod veuillez mettre à jour le mod - - + + mods repository index Index du dépôt de mods - + or newer ou plus récente - + Supported VCMI versions Versions supportées de VCMI - + Languages Langues - + Required mods Mods requis - + Conflicting mods Mods en conflit - This mod can not be installed or enabled because the following dependencies are not present - Ce mod ne peut pas être installé ou activé car les dépendances suivantes ne sont pas présents + Ce mod ne peut pas être installé ou activé car les dépendances suivantes ne sont pas présents - This mod can not be enabled because the following mods are incompatible with it - Ce mod ne peut pas être installé ou activé, car les dépendances suivantes sont incompatibles avec lui + Ce mod ne peut pas être installé ou activé, car les dépendances suivantes sont incompatibles avec lui - This mod cannot be disabled because it is required by the following mods - Ce mod ne peut pas être désactivé car il est requis pour les dépendances suivantes + Ce mod ne peut pas être désactivé car il est requis pour les dépendances suivantes - This mod cannot be uninstalled or updated because it is required by the following mods - Ce mod ne peut pas être désinstallé ou mis à jour car il est requis pour les dépendances suivantes + Ce mod ne peut pas être désinstallé ou mis à jour car il est requis pour les dépendances suivantes - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Ce sous-mod ne peut pas être installé ou mis à jour séparément du mod parent - + Notes Notes - + All supported files Tous les fichiers supportés - + Maps Cartes - + Campaigns Campagnes - + Configs Configurations - + Mods Mods - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? Remplacer le fichier de configuration ? - + Do you want to replace %1? Voulez vous remplacer %1 ? - + Downloading %1. %p% (%v MB out of %m MB) finished Téléchargement %1. %p% (%v Mo sur %m Mo) terminé - + Download failed Téléchargement échoué - + Unable to download all files. Encountered errors: @@ -493,7 +482,7 @@ Erreur rencontrées: - + Install successfully downloaded? @@ -502,39 +491,39 @@ Install successfully downloaded? Installer les téchargements réussis? - + Installing chronicles - + Installing mod %1 Installer le mod %1 - + Operation failed Opération échouée - + Encountered errors: Erreurs rencontrées: - + screenshots captures d'écran - + Screenshot %1 Impression écran %1 - + Mod is incompatible Ce mod est incompatible @@ -542,97 +531,77 @@ Installer les téchargements réussis? CModManager - Can not install submod - Impossible d'installer le sous-mod + Impossible d'installer le sous-mod - Mod is already installed - Le mod est déjà installé + Le mod est déjà installé - Can not uninstall submod - Impossible de désinstaller le sousmod + Impossible de désinstaller le sousmod - Mod is not installed - Le mod n'est pas installé + Le mod n'est pas installé - Mod is already enabled - Mod déjà activé + Mod déjà activé - - Mod must be installed first - Le mode doit d'abord être installé + Le mode doit d'abord être installé - Mod is not compatible, please update VCMI and checkout latest mod revisions - Mod non compatible, veuillez mettre à jour VCMI et vérifier la dernière revision du mod + Mod non compatible, veuillez mettre à jour VCMI et vérifier la dernière revision du mod - Required mod %1 is missing - Le mod requis %1 est manquant + Le mod requis %1 est manquant - Required mod %1 is not enabled - Le mod requis %1 n'est pas activé + Le mod requis %1 n'est pas activé - - This mod conflicts with %1 - Ce mod rentre en conflit avec %1 + Ce mod rentre en conflit avec %1 - Mod is already disabled - Mod déjà désactivé + Mod déjà désactivé - This mod is needed to run %1 - Le mod est requis pour lancer %1 + Le mod est requis pour lancer %1 - Mod archive is missing - Archive du mod manquante + Archive du mod manquante - Mod with such name is already installed - Un mod avec le même nom est déjà installé + Un mod avec le même nom est déjà installé - Mod archive is invalid or corrupted - L'archive du mod est invalide ou corrompue + L'archive du mod est invalide ou corrompue - Failed to extract mod data - Echec de l'extraction des données du mod + Echec de l'extraction des données du mod - Data with this mod was not found - Les données de ce mod n'ont pas étés trouvées + Les données de ce mod n'ont pas étés trouvées - Mod is located in protected directory, please remove it manually: - Le mod est placé dans un dossier protégé, veuillez le supprimer manuellement: + Le mod est placé dans un dossier protégé, veuillez le supprimer manuellement: @@ -1098,29 +1067,26 @@ Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écra File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1570,16 +1536,209 @@ Raison de l'erreur : ModFields - + Name Nom - + Type Type + + ModStateController + + + Can not install submod + Impossible d'installer le sous-mod + + + + Mod is already installed + Le mod est déjà installé + + + + Can not uninstall submod + Impossible de désinstaller le sousmod + + + + Mod is not installed + Le mod n'est pas installé + + + + Mod is already enabled + Mod déjà activé + + + + + Mod must be installed first + Le mode doit d'abord être installé + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Mod non compatible, veuillez mettre à jour VCMI et vérifier la dernière revision du mod + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Le mod requis %1 est manquant + + + + Mod is already disabled + Mod déjà désactivé + + + + Mod archive is missing + Archive du mod manquante + + + + Mod with such name is already installed + Un mod avec le même nom est déjà installé + + + + Mod archive is invalid or corrupted + L'archive du mod est invalide ou corrompue + + + + Failed to extract mod data + Echec de l'extraction des données du mod + + + + Data with this mod was not found + Les données de ce mod n'ont pas étés trouvées + + + + Mod is located in protected directory, please remove it manually: + + Le mod est placé dans un dossier protégé, veuillez le supprimer manuellement: + + + + + ModStateItemModel + + + Translation + Traduction + + + + Town + Ville + + + + Test + Test + + + + Templates + Modèles + + + + Spells + Sorts + + + + Music + Musique + + + + Maps + Cartes + + + + Sounds + Sons + + + + Skills + Compétences + + + + + Other + Autre + + + + Objects + Objets + + + + + Mechanics + Mécaniques + + + + + Interface + Interface + + + + Heroes + Héros + + + + + Graphical + Graphisme + + + + Expansion + Extension + + + + Creatures + Créatures + + + + Compatibility + Compatibilité + + + + Artifacts + Artefacts + + + + AI + IA + + QObject diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index 4491995a6..e1c0f8068 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Übersetzung + Übersetzung - Town - Stadt + Stadt - Test - Test + Test - Templates - Templates + Templates - Spells - Zaubersprüche + Zaubersprüche - Music - Musik + Musik - Maps - Karten + Karten - Sounds - Sounds + Sounds - Skills - Fertigkeiten + Fertigkeiten - - Other - Andere + Andere - Objects - Objekte + Objekte - - Mechanics - Mechaniken + Mechaniken - - Interface - Schnittstelle + Schnittstelle - Heroes - Helden + Helden - - Graphical - Grafisches + Grafisches - Expansion - Erweiterung + Erweiterung - Creatures - Kreaturen + Kreaturen - Compatibility - Kompatibilität + Kompatibilität - Artifacts - Artefakte + Artefakte - AI - KI + KI @@ -233,7 +209,7 @@ - + Description Beschreibung @@ -293,189 +269,202 @@ Abbrechen - + Mod name Mod-Name - + + Installed version Installierte Version - + + Latest version Letzte Version - + Size Größe - + Download size Downloadgröße - + Authors Autoren - + License Lizenz - + Contact Kontakt - + Compatibility Kompatibilität - - + + Required VCMI version Benötigte VCMI Version - + Supported VCMI version Unterstützte VCMI Version - + please upgrade mod bitte Mod upgraden - - + + mods repository index Mod Verzeichnis Index - + or newer oder neuer - + Supported VCMI versions Unterstützte VCMI Versionen - + Languages Sprachen - + Required mods Benötigte Mods - + Conflicting mods Mods mit Konflikt - This mod can not be installed or enabled because the following dependencies are not present - Diese Mod kann nicht installiert oder aktiviert werden, da die folgenden Abhängigkeiten nicht vorhanden sind + Diese Mod kann nicht installiert oder aktiviert werden, da die folgenden Abhängigkeiten nicht vorhanden sind - This mod can not be enabled because the following mods are incompatible with it - Diese Mod kann nicht aktiviert werden, da folgende Mods nicht mit dieser Mod kompatibel sind + Diese Mod kann nicht aktiviert werden, da folgende Mods nicht mit dieser Mod kompatibel sind - This mod cannot be disabled because it is required by the following mods - Diese Mod kann nicht deaktiviert werden, da sie zum Ausführen der folgenden Mods erforderlich ist + Diese Mod kann nicht deaktiviert werden, da sie zum Ausführen der folgenden Mods erforderlich ist - This mod cannot be uninstalled or updated because it is required by the following mods - Diese Mod kann nicht deinstalliert oder aktualisiert werden, da sie für die folgenden Mods erforderlich ist + Diese Mod kann nicht deinstalliert oder aktualisiert werden, da sie für die folgenden Mods erforderlich ist - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Dies ist eine Submod und kann nicht separat von der Hauptmod installiert oder deinstalliert werden - + Notes Anmerkungen - + All supported files Alle unterstützten Dateien - + Maps Karten - + Campaigns Kampagnen - + Configs Konfigurationen - + Mods Mods - + Gog files Gog-Dateien - + All files (*.*) Alle Dateien (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... Wähle zu installierenden Dateien aus (Konfigs, Mods, Karten, Kampagnen, Gog-Dateien)... - + Replace config file? Konfigurationsdatei ersetzen? - + Do you want to replace %1? Soll %1 ersetzt werden? - + Downloading %1. %p% (%v MB out of %m MB) finished Downloade %1. %p% (%v MB von %m MB) abgeschlossen - + Download failed Download fehlgeschlagen - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Es sind Fehler aufgetreten: - + Install successfully downloaded? @@ -497,39 +486,39 @@ Install successfully downloaded? Installation erfolgreich heruntergeladen? - + Installing chronicles Installation der Chronicles - + Installing mod %1 Installation von Mod %1 - + Operation failed Operation fehlgeschlagen - + Encountered errors: Aufgetretene Fehler: - + screenshots Screenshots - + Screenshot %1 Screenshot %1 - + Mod is incompatible Mod ist inkompatibel @@ -537,97 +526,77 @@ Installation erfolgreich heruntergeladen? CModManager - Can not install submod - Submod kann nicht installiert werden + Submod kann nicht installiert werden - Mod is already installed - Mod ist bereits installiert + Mod ist bereits installiert - Can not uninstall submod - Submod kann nicht deinstalliert werden + Submod kann nicht deinstalliert werden - Mod is not installed - Mod ist nicht installiert + Mod ist nicht installiert - Mod is already enabled - Mod ist bereits aktiviert + Mod ist bereits aktiviert - - Mod must be installed first - Mod muss zuerst installiert werden + Mod muss zuerst installiert werden - Mod is not compatible, please update VCMI and checkout latest mod revisions - Mod ist nicht kompatibel, bitte aktualisieren Sie VCMI und überprüfen Sie die neuesten Mod-Versionen + Mod ist nicht kompatibel, bitte aktualisieren Sie VCMI und überprüfen Sie die neuesten Mod-Versionen - Required mod %1 is missing - Der erforderliche Mod %1 fehlt + Der erforderliche Mod %1 fehlt - Required mod %1 is not enabled - Erforderliche Mod %1 ist nicht aktiviert + Erforderliche Mod %1 ist nicht aktiviert - - This mod conflicts with %1 - Diese Mod steht im Konflikt mit %1 + Diese Mod steht im Konflikt mit %1 - Mod is already disabled - Mod ist bereits deaktiviert + Mod ist bereits deaktiviert - This mod is needed to run %1 - Diese Mod wird benötigt, um %1 auszuführen + Diese Mod wird benötigt, um %1 auszuführen - Mod archive is missing - Mod-Archiv fehlt + Mod-Archiv fehlt - Mod with such name is already installed - Mod mit diesem Namen ist bereits installiert + Mod mit diesem Namen ist bereits installiert - Mod archive is invalid or corrupted - Mod-Archiv ist ungültig oder beschädigt + Mod-Archiv ist ungültig oder beschädigt - Failed to extract mod data - Mod-Daten konnten nicht extrahiert werden + Mod-Daten konnten nicht extrahiert werden - Data with this mod was not found - Daten mit dieser Mod wurden nicht gefunden + Daten mit dieser Mod wurden nicht gefunden - Mod is located in protected directory, please remove it manually: - Mod befindet sich im geschützten Verzeichnis, bitte entfernen Sie sie manuell: + Mod befindet sich im geschützten Verzeichnis, bitte entfernen Sie sie manuell: @@ -1093,29 +1062,26 @@ Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwend File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1565,16 +1531,209 @@ Fehlerursache: ModFields - + Name Name - + Type Typ + + ModStateController + + + Can not install submod + Submod kann nicht installiert werden + + + + Mod is already installed + Mod ist bereits installiert + + + + Can not uninstall submod + Submod kann nicht deinstalliert werden + + + + Mod is not installed + Mod ist nicht installiert + + + + Mod is already enabled + Mod ist bereits aktiviert + + + + + Mod must be installed first + Mod muss zuerst installiert werden + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Mod ist nicht kompatibel, bitte aktualisieren Sie VCMI und überprüfen Sie die neuesten Mod-Versionen + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Der erforderliche Mod %1 fehlt + + + + Mod is already disabled + Mod ist bereits deaktiviert + + + + Mod archive is missing + Mod-Archiv fehlt + + + + Mod with such name is already installed + Mod mit diesem Namen ist bereits installiert + + + + Mod archive is invalid or corrupted + Mod-Archiv ist ungültig oder beschädigt + + + + Failed to extract mod data + Mod-Daten konnten nicht extrahiert werden + + + + Data with this mod was not found + Daten mit dieser Mod wurden nicht gefunden + + + + Mod is located in protected directory, please remove it manually: + + Mod befindet sich im geschützten Verzeichnis, bitte entfernen Sie sie manuell: + + + + + ModStateItemModel + + + Translation + Übersetzung + + + + Town + Stadt + + + + Test + Test + + + + Templates + Templates + + + + Spells + Zaubersprüche + + + + Music + Musik + + + + Maps + Karten + + + + Sounds + Sounds + + + + Skills + Fertigkeiten + + + + + Other + Andere + + + + Objects + Objekte + + + + + Mechanics + Mechaniken + + + + + Interface + Schnittstelle + + + + Heroes + Helden + + + + + Graphical + Grafisches + + + + Expansion + Erweiterung + + + + Creatures + Kreaturen + + + + Compatibility + Kompatibilität + + + + Artifacts + Artefakte + + + + AI + KI + + QObject diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 75a62d829..154c2bf1d 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Tłumaczenie + Tłumaczenie - Town - Miasto + Miasto - Test - Test + Test - Templates - Szablony + Szablony - Spells - Zaklęcia + Zaklęcia - Music - Muzyczny + Muzyczny - Maps - Mapy + Mapy - Sounds - Dźwięki + Dźwięki - Skills - Umiejętności + Umiejętności - - Other - Inne + Inne - Objects - Obiekty + Obiekty - - Mechanics - Mechaniki + Mechaniki - - Interface - Interfejs + Interfejs - Heroes - Bohaterowie + Bohaterowie - - Graphical - Graficzny + Graficzny - Expansion - Dodatek + Dodatek - Creatures - Stworzenia + Stworzenia - Compatibility - Kompatybilność + Kompatybilność - Artifacts - Artefakty + Artefakty - AI - AI + AI @@ -233,7 +209,7 @@ - + Description Opis @@ -293,189 +269,202 @@ Przerwij - + Mod name Nazwa moda - + + Installed version Zainstalowana wersja - + + Latest version Najnowsza wersja - + Size Rozmiar - + Download size Rozmiar pobierania - + Authors Autorzy - + License Licencja - + Contact Kontakt - + Compatibility Kompatybilność - - + + Required VCMI version Wymagana wersja VCMI - + Supported VCMI version Wspierana wersja VCMI - + please upgrade mod proszę zaktualizować moda - - + + mods repository index indeks repozytorium modów - + or newer lub nowsze - + Supported VCMI versions Wspierane wersje VCMI - + Languages Języki - + Required mods Wymagane mody - + Conflicting mods Konfliktujące mody - This mod can not be installed or enabled because the following dependencies are not present - Ten mod nie może zostać zainstalowany lub włączony ponieważ następujące zależności nie zostały spełnione + Ten mod nie może zostać zainstalowany lub włączony ponieważ następujące zależności nie zostały spełnione - This mod can not be enabled because the following mods are incompatible with it - Ten mod nie może zostać włączony ponieważ następujące mody są z nim niekompatybilne + Ten mod nie może zostać włączony ponieważ następujące mody są z nim niekompatybilne - This mod cannot be disabled because it is required by the following mods - Ten mod nie może zostać wyłączony ponieważ jest wymagany do uruchomienia następujących modów + Ten mod nie może zostać wyłączony ponieważ jest wymagany do uruchomienia następujących modów - This mod cannot be uninstalled or updated because it is required by the following mods - Ten mod nie może zostać odinstalowany lub zaktualizowany ponieważ jest wymagany do uruchomienia następujących modów + Ten mod nie może zostać odinstalowany lub zaktualizowany ponieważ jest wymagany do uruchomienia następujących modów - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod To jest moduł składowy innego moda i nie może być zainstalowany lub odinstalowany oddzielnie od moda nadrzędnego - + Notes Uwagi - + All supported files Wszystkie wspierane pliki - + Maps Mapy - + Campaigns Kampanie - + Configs Konfiguracje - + Mods Mody - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? Zastąpić plik konfiguracji? - + Do you want to replace %1? Czy chcesz zastąpić %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Pobieranie %1. %p% (%v MB z %m MB) ukończono - + Download failed Pobieranie nieudane - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Napotkane błędy: - + Install successfully downloaded? @@ -497,39 +486,39 @@ Install successfully downloaded? Zainstalować pomyślnie pobrane? - + Installing chronicles - + Installing mod %1 Instalowanie modyfikacji %1 - + Operation failed Operacja nieudana - + Encountered errors: Napotkane błędy: - + screenshots zrzuty ekranu - + Screenshot %1 Zrzut ekranu %1 - + Mod is incompatible Mod jest niekompatybilny @@ -537,97 +526,77 @@ Zainstalować pomyślnie pobrane? CModManager - Can not install submod - Nie można zainstalować submoda + Nie można zainstalować submoda - Mod is already installed - Mod jest już zainstalowany + Mod jest już zainstalowany - Can not uninstall submod - Nie można odinstalować submoda + Nie można odinstalować submoda - Mod is not installed - Mod nie jest zainstalowany + Mod nie jest zainstalowany - Mod is already enabled - Mod jest już włączony + Mod jest już włączony - - Mod must be installed first - Mod musi zostać najpierw zainstalowany + Mod musi zostać najpierw zainstalowany - Mod is not compatible, please update VCMI and checkout latest mod revisions - Mod nie jest kompatybilny, proszę zaktualizować VCMI i odświeżyć listę modów + Mod nie jest kompatybilny, proszę zaktualizować VCMI i odświeżyć listę modów - Required mod %1 is missing - Brakuje wymaganego moda %1 + Brakuje wymaganego moda %1 - Required mod %1 is not enabled - Wymagany mod %1 jest wyłączony + Wymagany mod %1 jest wyłączony - - This mod conflicts with %1 - Ten mod konfliktuje z %1 + Ten mod konfliktuje z %1 - Mod is already disabled - Mod jest już wyłączony + Mod jest już wyłączony - This mod is needed to run %1 - Ten mod jest potrzebny do uruchomienia %1 + Ten mod jest potrzebny do uruchomienia %1 - Mod archive is missing - Brakuje archiwum modyfikacji + Brakuje archiwum modyfikacji - Mod with such name is already installed - Mod z taką nazwą jest już zainstalowany + Mod z taką nazwą jest już zainstalowany - Mod archive is invalid or corrupted - Archiwum moda jest niepoprawne lub uszkodzone + Archiwum moda jest niepoprawne lub uszkodzone - Failed to extract mod data - Nieudane wyodrębnienie danych moda + Nieudane wyodrębnienie danych moda - Data with this mod was not found - Dane z tym modem nie zostały znalezione + Dane z tym modem nie zostały znalezione - Mod is located in protected directory, please remove it manually: - Mod jest umiejscowiony w chronionym folderze, proszę go usunąć ręcznie: + Mod jest umiejscowiony w chronionym folderze, proszę go usunąć ręcznie: @@ -1093,29 +1062,26 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1565,16 +1531,209 @@ powód błędu: ModFields - + Name Nazwa - + Type Typ + + ModStateController + + + Can not install submod + Nie można zainstalować submoda + + + + Mod is already installed + Mod jest już zainstalowany + + + + Can not uninstall submod + Nie można odinstalować submoda + + + + Mod is not installed + Mod nie jest zainstalowany + + + + Mod is already enabled + Mod jest już włączony + + + + + Mod must be installed first + Mod musi zostać najpierw zainstalowany + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Mod nie jest kompatybilny, proszę zaktualizować VCMI i odświeżyć listę modów + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Brakuje wymaganego moda %1 + + + + Mod is already disabled + Mod jest już wyłączony + + + + Mod archive is missing + Brakuje archiwum modyfikacji + + + + Mod with such name is already installed + Mod z taką nazwą jest już zainstalowany + + + + Mod archive is invalid or corrupted + Archiwum moda jest niepoprawne lub uszkodzone + + + + Failed to extract mod data + Nieudane wyodrębnienie danych moda + + + + Data with this mod was not found + Dane z tym modem nie zostały znalezione + + + + Mod is located in protected directory, please remove it manually: + + Mod jest umiejscowiony w chronionym folderze, proszę go usunąć ręcznie: + + + + + ModStateItemModel + + + Translation + Tłumaczenie + + + + Town + Miasto + + + + Test + Test + + + + Templates + Szablony + + + + Spells + Zaklęcia + + + + Music + Muzyczny + + + + Maps + Mapy + + + + Sounds + Dźwięki + + + + Skills + Umiejętności + + + + + Other + Inne + + + + Objects + Obiekty + + + + + Mechanics + Mechaniki + + + + + Interface + Interfejs + + + + Heroes + Bohaterowie + + + + + Graphical + Graficzny + + + + Expansion + Dodatek + + + + Creatures + Stworzenia + + + + Compatibility + Kompatybilność + + + + Artifacts + Artefakty + + + + AI + AI + + QObject diff --git a/launcher/translation/portuguese.ts b/launcher/translation/portuguese.ts index 2dd4103cf..a01983547 100644 --- a/launcher/translation/portuguese.ts +++ b/launcher/translation/portuguese.ts @@ -87,113 +87,6 @@ Relatar um erro - - CModListModel - - - Translation - Tradução - - - - Town - Cidade - - - - Test - Teste - - - - Templates - Modelos - - - - Spells - Feitiços - - - - Music - Música - - - - Maps - Mapas - - - - Sounds - Sons - - - - Skills - Habilidades - - - - - Other - Outros - - - - Objects - Objetos - - - - - Mechanics - Mecânicas - - - - - Interface - Interface - - - - Heroes - Heróis - - - - - Graphical - Gráficos - - - - Expansion - Expansão - - - - Creatures - Criaturas - - - - Compatibility - Compatibilidade - - - - Artifacts - Artefatos - - - - AI - IA - - CModListView @@ -233,7 +126,7 @@ - + Description Descrição @@ -293,189 +186,186 @@ Cancelar - + Mod name Nome do mod - + + Installed version Versão instalada - + + Latest version Última versão - + Size Tamanho - + Download size Tamanho do download - + Authors Autores - + License Licença - + Contact Contato - + Compatibility Compatibilidade - - + + Required VCMI version Versão do VCMI necessária - + Supported VCMI version Versão do VCMI suportada - + please upgrade mod por favor, atualize o mod - - + + mods repository index índice do repositório de mods - + or newer ou mais recente - + Supported VCMI versions Versões do VCMI suportadas - + Languages Idiomas - + Required mods Mods requeridos - + Conflicting mods Mods conflitantes - - This mod can not be installed or enabled because the following dependencies are not present - Este mod não pode ser instalado ou ativado porque as seguintes dependências não estão presentes + + This mod cannot be enabled because it translates into a different language. + Este mod não pode ser ativado porque traduz para um idioma diferente. - - This mod can not be enabled because the following mods are incompatible with it - Este mod não pode ser ativado porque os seguintes mods são incompatíveis com ele + + This mod can not be enabled because the following dependencies are not present + Este mod não pode ser ativado porque as seguintes dependências estão ausentes - - This mod cannot be disabled because it is required by the following mods - Este mod não pode ser desativado porque é necessário pelos seguintes mods + + This mod can not be installed because the following dependencies are not present + Este mod não pode ser instalado porque as seguintes dependências estão ausentes - - This mod cannot be uninstalled or updated because it is required by the following mods - Este mod não pode ser desinstalado ou atualizado porque é necessário pelos seguintes mods - - - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Este é um submod e não pode ser instalado ou desinstalado separadamente do seu mod principal - + Notes Notas - + All supported files Todos os arquivos suportados - + Maps Mapas - + Campaigns Campanhas - + Configs Configurações - + Mods Mods - + Gog files Arquivos GOG - + All files (*.*) Todos os arquivos (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - Selecione arquivos (configurações, mods, mapas, campanhas, arquivos gog) para instalar... + Selecione arquivos (configurações, mods, mapas, campanhas, arquivos GOG) para instalar... - + Replace config file? Substituir arquivo de configuração? - + Do you want to replace %1? Você deseja substituir %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Baixando %1. %p% (%v MB de %m MB) concluído - + Download failed Falha no download - + Unable to download all files. Encountered errors: @@ -488,7 +378,7 @@ Erros encontrados: - + Install successfully downloaded? @@ -497,140 +387,43 @@ Install successfully downloaded? O download da instalação foi bem-sucedido? - + Installing chronicles Instalando crônicas - + Installing mod %1 Instalando mod %1 - + Operation failed Falha na operação - + Encountered errors: Erros encontrados: - + screenshots capturas de tela - + Screenshot %1 Captura de tela %1 - + Mod is incompatible O mod é incompatível - - CModManager - - - Can not install submod - Não é possível instalar o submod - - - - Mod is already installed - O mod já está instalado - - - - Can not uninstall submod - Não é possível desinstalar o submod - - - - Mod is not installed - O mod não está instalado - - - - Mod is already enabled - O mod já está ativado - - - - - Mod must be installed first - O mod deve ser instalado primeiro - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - O mod não é compatível, por favor, atualize o VCMI e verifique as últimas revisões do mod - - - - Required mod %1 is missing - O mod necessário %1 está faltando - - - - Required mod %1 is not enabled - O mod necessário %1 não está ativado - - - - - This mod conflicts with %1 - Este mod entra em conflito com %1 - - - - Mod is already disabled - O mod já está desativado - - - - This mod is needed to run %1 - Este mod é necessário para executar %1 - - - - Mod archive is missing - O arquivo do mod está faltando - - - - Mod with such name is already installed - Um mod com esse nome já está instalado - - - - Mod archive is invalid or corrupted - O arquivo do mod é inválido ou está corrompido - - - - Failed to extract mod data - Falha ao extrair os dados do mod - - - - Data with this mod was not found - Não foram encontrados dados com este mod - - - - Mod is located in protected directory, please remove it manually: - - O mod está localizado em um diretório protegido, por favor, remova-o manualmente: - - - CSettingsView @@ -1075,7 +868,7 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu You have to select an chronicle installer file! - Você precisa selecionar um arquivo de instalação do Chronicles! + Você precisa selecionar um arquivo de instalação do Heroes Chronicles! @@ -1093,30 +886,11 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu File size - - %1 B - %1 B - - - - %1 KiB - %1 KiB - - - + + %1 MiB %1 MiB - - - %1 GiB - %1 GiB - - - - %1 TiB - %1 TiB - FirstLaunchView @@ -1565,16 +1339,209 @@ Motivo do erro: ModFields - + Name Nome - + Type Tipo + + ModStateController + + + Can not install submod + Não é possível instalar o submod + + + + Mod is already installed + O mod já está instalado + + + + Can not uninstall submod + Não é possível desinstalar o submod + + + + Mod is not installed + O mod não está instalado + + + + Mod is already enabled + O mod já está ativado + + + + + Mod must be installed first + O mod deve ser instalado primeiro + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + O mod não é compatível, por favor, atualize o VCMI e verifique as últimas revisões do mod + + + + Can not enable translation mod for a different language! + Não é possível ativar o mod de tradução para um idioma diferente! + + + + Required mod %1 is missing + O mod necessário %1 está faltando + + + + Mod is already disabled + O mod já está desativado + + + + Mod archive is missing + O arquivo do mod está faltando + + + + Mod with such name is already installed + Um mod com esse nome já está instalado + + + + Mod archive is invalid or corrupted + O arquivo do mod é inválido ou está corrompido + + + + Failed to extract mod data + Falha ao extrair os dados do mod + + + + Data with this mod was not found + Não foram encontrados dados com este mod + + + + Mod is located in protected directory, please remove it manually: + + O mod está localizado em um diretório protegido, por favor, remova-o manualmente: + + + + + ModStateItemModel + + + Translation + Tradução + + + + Town + Cidade + + + + Test + Teste + + + + Templates + Modelos + + + + Spells + Feitiços + + + + Music + Música + + + + Maps + Mapas + + + + Sounds + Sons + + + + Skills + Habilidades + + + + + Other + Outros + + + + Objects + Objetos + + + + + Mechanics + Mecânicas + + + + + Interface + Interface + + + + Heroes + Heróis + + + + + Graphical + Gráficos + + + + Expansion + Expansão + + + + Creatures + Criaturas + + + + Compatibility + Compatibilidade + + + + Artifacts + Artefatos + + + + AI + IA + + QObject diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index d1f2acfa0..c30057e7c 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -90,108 +90,80 @@ CModListModel - Translation - Перевод + Перевод - Town - Город + Город - Test - Тест + Тест - Templates - Шаблоны карт + Шаблоны карт - Spells - Заклинания + Заклинания - Music - Музыка + Музыка - - Maps - - - - Sounds - Звуки + Звуки - Skills - Навыки + Навыки - - Other - Иное + Иное - Objects - Объекты + Объекты - - Mechanics - Механика + Механика - - Interface - Интерфейс + Интерфейс - Heroes - Герои + Герои - - Graphical - Графика + Графика - Expansion - Дополнение + Дополнение - Creatures - Существа + Существа - Compatibility - Совместимость + Совместимость - Artifacts - Артефакт + Артефакт - AI - ИИ + ИИ @@ -233,7 +205,7 @@ - + Description Описание @@ -293,189 +265,202 @@ Отмена - + Mod name Название мода - + + Installed version Установленная версия - + + Latest version Последняя версия - + Size - + Download size Размер загрузки - + Authors Авторы - + License Лицензия - + Contact Контакты - + Compatibility Совместимость - - + + Required VCMI version Требуемая версия VCMI - + Supported VCMI version Поддерживаемая версия VCMI - + please upgrade mod - - + + mods repository index - + or newer - + Supported VCMI versions Поддерживаемые версии VCMI - + Languages Языки - + Required mods Зависимости - + Conflicting mods Конфликтующие моды - This mod can not be installed or enabled because the following dependencies are not present - Этот мод не может быть установлен или активирован, так как отсутствуют следующие зависимости + Этот мод не может быть установлен или активирован, так как отсутствуют следующие зависимости - This mod can not be enabled because the following mods are incompatible with it - Этот мод не может быть установлен или активирован, так как следующие моды несовместимы с этим + Этот мод не может быть установлен или активирован, так как следующие моды несовместимы с этим - This mod cannot be disabled because it is required by the following mods - Этот мод не может быть выключен, так как он является зависимостью для следующих + Этот мод не может быть выключен, так как он является зависимостью для следующих - This mod cannot be uninstalled or updated because it is required by the following mods - Этот мод не может быть удален или обновлен, так как является зависимостью для следующих модов + Этот мод не может быть удален или обновлен, так как является зависимостью для следующих модов - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Это вложенный мод, он не может быть установлен или удален отдельно от родительского - + Notes Замечания - + All supported files - + Maps - + Campaigns - + Configs - + Mods Моды - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? - + Do you want to replace %1? - + Downloading %1. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -484,145 +469,49 @@ Encountered errors: - + Install successfully downloaded? - + Installing chronicles - + Installing mod %1 - + Operation failed - + Encountered errors: - + screenshots - + Screenshot %1 Скриншот %1 - + Mod is incompatible Мод несовместим - - CModManager - - - Can not install submod - - - - - Mod is already installed - - - - - Can not uninstall submod - - - - - Mod is not installed - - - - - Mod is already enabled - - - - - - Mod must be installed first - - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - - - - - Required mod %1 is missing - - - - - Required mod %1 is not enabled - - - - - - This mod conflicts with %1 - - - - - Mod is already disabled - - - - - This mod is needed to run %1 - - - - - Mod archive is missing - - - - - Mod with such name is already installed - - - - - Mod archive is invalid or corrupted - - - - - Failed to extract mod data - - - - - Data with this mod was not found - - - - - Mod is located in protected directory, please remove it manually: - - - - CSettingsView @@ -1079,30 +968,11 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1546,16 +1416,208 @@ error reason: ModFields - + Name Название - + Type Тип + + ModStateController + + + Can not install submod + + + + + Mod is already installed + + + + + Can not uninstall submod + + + + + Mod is not installed + + + + + Mod is already enabled + + + + + + Mod must be installed first + + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + + + + + Mod is already disabled + + + + + Mod archive is missing + + + + + Mod with such name is already installed + + + + + Mod archive is invalid or corrupted + + + + + Failed to extract mod data + + + + + Data with this mod was not found + + + + + Mod is located in protected directory, please remove it manually: + + + + + + ModStateItemModel + + + Translation + Перевод + + + + Town + Город + + + + Test + Тест + + + + Templates + Шаблоны карт + + + + Spells + Заклинания + + + + Music + Музыка + + + + Maps + + + + + Sounds + Звуки + + + + Skills + Навыки + + + + + Other + Иное + + + + Objects + Объекты + + + + + Mechanics + Механика + + + + + Interface + Интерфейс + + + + Heroes + Герои + + + + + Graphical + Графика + + + + Expansion + Дополнение + + + + Creatures + Существа + + + + Compatibility + Совместимость + + + + Artifacts + Артефакт + + + + AI + ИИ + + QObject diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index 7573df603..26821adc9 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Traducción + Traducción - Town - Ciudad + Ciudad - Test - Test + Test - Templates - Plantillas + Plantillas - Spells - Hechizos + Hechizos - Music - Música + Música - Maps - Mapas + Mapas - Sounds - Sonidos + Sonidos - Skills - Habilidades + Habilidades - - Other - Otro + Otro - Objects - Objetos + Objetos - - Mechanics - Mecánicas + Mecánicas - - Interface - Interfaz + Interfaz - Heroes - Heroes + Heroes - - Graphical - Gráficos + Gráficos - Expansion - Expansión + Expansión - Creatures - Criaturas + Criaturas - Compatibility - Compatibilidad + Compatibilidad - Artifacts - Artefactos + Artefactos - AI - IA + IA @@ -233,7 +209,7 @@ - + Description Descripción @@ -293,189 +269,202 @@ Cancelar - + Mod name Nombre del mod - + + Installed version Versión instalada - + + Latest version Última versión - + Size Tamaño - + Download size Tamaño de descarga - + Authors Autores - + License Licencia - + Contact Contacto - + Compatibility Compatibilidad - - + + Required VCMI version Versión de VCMI requerida - + Supported VCMI version Versión de VCMI compatible - + please upgrade mod - - + + mods repository index - + or newer - + Supported VCMI versions Versiones de VCMI compatibles - + Languages Idiomas - + Required mods Mods requeridos - + Conflicting mods Mods conflictivos - This mod can not be installed or enabled because the following dependencies are not present - Este mod no se puede instalar o habilitar porque no están presentes las siguientes dependencias + Este mod no se puede instalar o habilitar porque no están presentes las siguientes dependencias - This mod can not be enabled because the following mods are incompatible with it - Este mod no se puede habilitar porque los siguientes mods son incompatibles con él + Este mod no se puede habilitar porque los siguientes mods son incompatibles con él - This mod cannot be disabled because it is required by the following mods - No se puede desactivar este mod porque es necesario para ejecutar los siguientes mods + No se puede desactivar este mod porque es necesario para ejecutar los siguientes mods - This mod cannot be uninstalled or updated because it is required by the following mods - No se puede desinstalar o actualizar este mod porque es necesario para ejecutar los siguientes mods + No se puede desinstalar o actualizar este mod porque es necesario para ejecutar los siguientes mods - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Este es un submod y no se puede instalar o desinstalar por separado del mod principal - + Notes Notas - + All supported files - + Maps Mapas - + Campaigns - + Configs - + Mods Mods - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? - + Do you want to replace %1? - + Downloading %1. %p% (%v MB out of %m MB) finished - + Download failed Descarga fallida - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Errores encontrados: - + Install successfully downloaded? @@ -497,139 +486,43 @@ Install successfully downloaded? Instalar lo correctamente descargado? - + Installing chronicles - + Installing mod %1 Instalando mod %1 - + Operation failed Operación fallida - + Encountered errors: Errores encontrados: - + screenshots - + Screenshot %1 Captura de pantalla %1 - + Mod is incompatible El mod es incompatible - - CModManager - - - Can not install submod - - - - - Mod is already installed - - - - - Can not uninstall submod - - - - - Mod is not installed - - - - - Mod is already enabled - - - - - - Mod must be installed first - - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - - - - - Required mod %1 is missing - - - - - Required mod %1 is not enabled - - - - - - This mod conflicts with %1 - - - - - Mod is already disabled - - - - - This mod is needed to run %1 - - - - - Mod archive is missing - - - - - Mod with such name is already installed - - - - - Mod archive is invalid or corrupted - - - - - Failed to extract mod data - - - - - Data with this mod was not found - - - - - Mod is located in protected directory, please remove it manually: - - - - CSettingsView @@ -1092,30 +985,11 @@ Pantalla completa - el juego cubrirá la totalidad de la pantalla y utilizará l File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1559,16 +1433,208 @@ error reason: ModFields - + Name Nombre - + Type Tipo + + ModStateController + + + Can not install submod + + + + + Mod is already installed + + + + + Can not uninstall submod + + + + + Mod is not installed + + + + + Mod is already enabled + + + + + + Mod must be installed first + + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + + + + + Mod is already disabled + + + + + Mod archive is missing + + + + + Mod with such name is already installed + + + + + Mod archive is invalid or corrupted + + + + + Failed to extract mod data + + + + + Data with this mod was not found + + + + + Mod is located in protected directory, please remove it manually: + + + + + + ModStateItemModel + + + Translation + Traducción + + + + Town + Ciudad + + + + Test + Test + + + + Templates + Plantillas + + + + Spells + Hechizos + + + + Music + Música + + + + Maps + Mapas + + + + Sounds + Sonidos + + + + Skills + Habilidades + + + + + Other + Otro + + + + Objects + Objetos + + + + + Mechanics + Mecánicas + + + + + Interface + Interfaz + + + + Heroes + Heroes + + + + + Graphical + Gráficos + + + + Expansion + Expansión + + + + Creatures + Criaturas + + + + Compatibility + Compatibilidad + + + + Artifacts + Artefactos + + + + AI + IA + + QObject diff --git a/launcher/translation/swedish.ts b/launcher/translation/swedish.ts index 2bd48e043..dc27a26ff 100644 --- a/launcher/translation/swedish.ts +++ b/launcher/translation/swedish.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Översättning + Översättning - Town - Stad + Stad - Test - Test + Test - Templates - Modeller + Modeller - Spells - Trollformler + Trollformler - Music - Musik + Musik - Maps - Kartor + Kartor - Sounds - Ljud + Ljud - Skills - Färdigheter + Färdigheter - - Other - Övrigt + Annan - Objects - Objekt + Objekt - - Mechanics - Mekanik + Mekanik - - Interface - Gränssnitt + Gränssnitt - Heroes - Hjälte + Hjälte - - Graphical - Grafik + Grafik - Expansion - Expansion/Tillägg + Expansion/Tillägg - Creatures - Varelser + Varelser - Compatibility - Kompatibilitet + Kompatibilitet - Artifacts - Artefakter + Artefakter - AI - AI + AI @@ -238,7 +214,7 @@ - + Description Beskrivning @@ -293,189 +269,202 @@ Avbryt - + Mod name Modd-namn - + + Installed version Installerad version - + + Latest version Senaste version - + Size Storlek - + Download size Nedladdnings-storlek - + Authors Författare - + License Licens - + Contact Kontakt - + Compatibility Kompatibilitet - - + + Required VCMI version VCMI-version som krävs - + Supported VCMI version VCMI-version som stöds - + please upgrade mod vänligen uppdatera modd - - + + mods repository index Modd-repositorie-index - + or newer eller nyare - + Supported VCMI versions VCMI-versioner som stöds - + Languages Språk - + Required mods Moddar som krävs - + Conflicting mods Modd-konflikter - This mod can not be installed or enabled because the following dependencies are not present - Denna modd kan inte installeras eller aktiveras eftersom följande beroenden inte finns + Denna modd kan inte installeras eller aktiveras eftersom följande beroenden inte finns - This mod can not be enabled because the following mods are incompatible with it - Denna modd kan inte aktiveras eftersom följande moddar är inkompatibla med den + Denna modd kan inte aktiveras eftersom följande moddar är inkompatibla med den - This mod cannot be disabled because it is required by the following mods - Denna modd kan inte inaktiveras eftersom den krävs för följande modd + Denna modd kan inte inaktiveras eftersom den krävs för följande modd - This mod cannot be uninstalled or updated because it is required by the following mods - Denna modd kan inte avinstalleras eller uppdateras eftersom den krävs för följande modd + Denna modd kan inte avinstalleras eller uppdateras eftersom den krävs för följande modd - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Detta är en undermodd/submodd och den kan inte installeras eller avinstalleras separat från huvud-modden - + Notes Anteckningar - + All supported files Alla filer som stöds - + Maps Kartor - + Campaigns Kampanjer - + Configs Konfigurationer - + Mods Moddar - - - Gog files - - - All files (*.*) - + Gog files + GOG-filer - Select files (configs, mods, maps, campaigns, gog files) to install... - + All files (*.*) + Alla filer (*.*) - + + Select files (configs, mods, maps, campaigns, gog files) to install... + Välj filer (konfigurationsfiler, moddar, kartor, kampanjer och GOG-filer) som ska installeras... + + + Replace config file? Byt ut konfigurationsfilen? - + Do you want to replace %1? Vill du ersätta %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Laddar ner %1. %p% (%v MB av %m MB) slutfört - + Download failed Nedladdning misslyckades - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Fel påträffat: - + Install successfully downloaded? @@ -497,39 +486,39 @@ Install successfully downloaded? Installation framgångsrikt nedladdad? - + Installing chronicles - + Installera Chronicles - + Installing mod %1 Installera modd %1 - + Operation failed Åtgärden misslyckades - + Encountered errors: Fel påträffades: - + screenshots skärmdumpar - + Screenshot %1 Skärmbild %1 - + Mod is incompatible Denna modd är inkompatibel @@ -537,97 +526,77 @@ Installation framgångsrikt nedladdad? CModManager - Can not install submod - Det går inte att installera undermodd/submodd + Det går inte att installera undermodd/submodd - Mod is already installed - Modden är redan installerad + Modden är redan installerad - Can not uninstall submod - Det går inte att avinstallera undermodd/submodd + Det går inte att avinstallera undermodd/submodd - Mod is not installed - Modden är inte installerad + Modden är inte installerad - Mod is already enabled - Modden är redan aktiverad + Modden är redan aktiverad - - Mod must be installed first - Modden måste installeras först + Modden måste installeras först - Mod is not compatible, please update VCMI and checkout latest mod revisions - Modden är inte kompatibel. Vänligen uppdatera VCMI och kontrollera att du har den senaste kompatibla versionen av modden + Modden är inte kompatibel. Vänligen uppdatera VCMI och kontrollera att du har den senaste kompatibla versionen av modden - Required mod %1 is missing - Den obligatorisk modden %1 saknas + Den obligatorisk modden %1 saknas - Required mod %1 is not enabled - Den obligatoriska modden %1 är inte aktiverad + Den obligatoriska modden %1 är inte aktiverad - - This mod conflicts with %1 - Denna modd är i konflikt med %1 + Denna modd är i konflikt med %1 - Mod is already disabled - Modden är redan inaktiverad + Modden är redan inaktiverad - This mod is needed to run %1 - Denna modden krävs för att köra %1 + Denna modden krävs för att köra %1 - Mod archive is missing - Modd-arkiv saknas + Modd-arkiv saknas - Mod with such name is already installed - En modd med samma namn är redan installerad + En modd med samma namn är redan installerad - Mod archive is invalid or corrupted - Modd-arkivet är ogiltigt eller korrupt + Modd-arkivet är ogiltigt eller korrupt - Failed to extract mod data - Misslyckades att extrahera data från modd + Misslyckades att extrahera data från modd - Data with this mod was not found - Modd-data för denna modd hittades inte + Modd-data för denna modd hittades inte - Mod is located in protected directory, please remove it manually: - Modden är placerad i en skyddad mapp. Vänligen radera den manuellt: + Modden är placerad i en skyddad mapp. Vänligen radera den manuellt: @@ -704,7 +673,7 @@ Installation framgångsrikt nedladdad? Mods Validation - + Validering av moddar @@ -729,12 +698,12 @@ Installation framgångsrikt nedladdad? Full - + Hela Use scalable fonts - + Använd skalbara teckensnitt @@ -744,27 +713,27 @@ Installation framgångsrikt nedladdad? Cursor Scaling - + Skalning av markör Scalable - + Skalbar Miscellaneous - + Övrigt Font Scaling (experimental) - + Skalning av teckensnitt (experimentell) Original - + Original @@ -774,7 +743,7 @@ Installation framgångsrikt nedladdad? Basic - + Grundläggande @@ -1059,63 +1028,60 @@ Exklusivt helskärmsläge - spelet kommer att täcka hela skärmen och använda File cannot opened - + Filen kan inte öppnas Invalid file selected - Ogiltig fil vald + Ogiltig fil vald You have to select an gog installer file! - + Du måste välja en GOG-installationsfil! You have to select an chronicle installer file! - + Du måste välja en Chronicles-installationsfil! Extracting error! - Extraktionsfel! + Extraheringsfel! Heroes Chronicles - + Heroes Chronicles File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1420,18 +1386,18 @@ Vänligen välj en mapp som innehåller data från Heroes III: Complete Edition Stream error while extracting files! error reason: - Strömningsfel vid extrahering av filer! + Strömningsfel vid extrahering av filer! Orsak till fel: Not a supported Inno Setup installer! - Inno Setup-installationsprogrammet stöds inte! + Inno Setup-installationsprogrammet stöds inte! VCMI was compiled without innoextract support, which is needed to extract exe files! - + VCMI kompilerades utan stöd för innoextract, vilket behövs för att extrahera exe-filer! @@ -1563,16 +1529,209 @@ Orsak till fel: ModFields - + Name Namn - + Type Typ + + ModStateController + + + Can not install submod + Det går inte att installera undermodd/submodd + + + + Mod is already installed + Modden är redan installerad + + + + Can not uninstall submod + Det går inte att avinstallera undermodd/submodd + + + + Mod is not installed + Modden är inte installerad + + + + Mod is already enabled + Modden är redan aktiverad + + + + + Mod must be installed first + Modden måste installeras först + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Modden är inte kompatibel. Vänligen uppdatera VCMI och kontrollera att du har den senaste kompatibla versionen av modden + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Den obligatorisk modden %1 saknas + + + + Mod is already disabled + Modden är redan inaktiverad + + + + Mod archive is missing + Modd-arkiv saknas + + + + Mod with such name is already installed + En modd med samma namn är redan installerad + + + + Mod archive is invalid or corrupted + Modd-arkivet är ogiltigt eller korrupt + + + + Failed to extract mod data + Misslyckades att extrahera data från modd + + + + Data with this mod was not found + Modd-data för denna modd hittades inte + + + + Mod is located in protected directory, please remove it manually: + + Modden är placerad i en skyddad mapp. Vänligen radera den manuellt: + + + + + ModStateItemModel + + + Translation + Översättning + + + + Town + Stad + + + + Test + Test + + + + Templates + Modeller + + + + Spells + Trollformler + + + + Music + Musik + + + + Maps + Kartor + + + + Sounds + Ljud + + + + Skills + Färdigheter + + + + + Other + Övrigt + + + + Objects + Objekt + + + + + Mechanics + Mekanik + + + + + Interface + Gränssnitt + + + + Heroes + Hjälte + + + + + Graphical + Grafik + + + + Expansion + Expansion/Tillägg + + + + Creatures + Varelser + + + + Compatibility + Kompatibilitet + + + + Artifacts + Artefakter + + + + AI + AI + + QObject diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index 1d0563981..1dcaa034b 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Переклад + Переклад - Town - Місто + Місто - Test - Тестування + Тестування - Templates - Шаблони + Шаблони - Spells - Закляття + Закляття - Music - Музика + Музика - Maps - Мапи + Мапи - Sounds - Звуки + Звуки - Skills - Вміння + Вміння - - Other - Інше + Інше - Objects - Об'єкти + Об'єкти - - Mechanics - Механіки + Механіки - - Interface - Інтерфейс + Інтерфейс - Heroes - Герої + Герої - - Graphical - Графічний + Графічний - Expansion - Розширення + Розширення - Creatures - Істоти + Істоти - Compatibility - Сумісність + Сумісність - Artifacts - Артефакти + Артефакти - AI - ШІ + ШІ @@ -233,7 +209,7 @@ - + Description Опис @@ -293,189 +269,202 @@ Відмінити - + Mod name Назва модифікації - + + Installed version Встановлена версія - + + Latest version Найновіша версія - + Size Розмір - + Download size Розмір для завантаження - + Authors Автори - + License Ліцензія - + Contact Контакти - + Compatibility Сумісність - - + + Required VCMI version Необхідна версія VCMI - + Supported VCMI version Підтримувана версія VCMI - + please upgrade mod будь ласка, оновіть модифікацію - - + + mods repository index каталог модифікацій - + or newer або новіше - + Supported VCMI versions Підтримувані версії VCMI - + Languages Мови - + Required mods Необхідні модифікації - + Conflicting mods Конфліктуючі модифікації - This mod can not be installed or enabled because the following dependencies are not present - Цю модифікацію не можна встановити чи активувати, оскільки відсутні наступні залежності + Цю модифікацію не можна встановити чи активувати, оскільки відсутні наступні залежності - This mod can not be enabled because the following mods are incompatible with it - Цю модифікацію не можна ввімкнути, оскільки наступні модифікації несумісні з цією модифікацією + Цю модифікацію не можна ввімкнути, оскільки наступні модифікації несумісні з цією модифікацією - This mod cannot be disabled because it is required by the following mods - Цю модифікацію не можна відключити, оскільки вона необхідна для запуску наступних модифікацій + Цю модифікацію не можна відключити, оскільки вона необхідна для запуску наступних модифікацій - This mod cannot be uninstalled or updated because it is required by the following mods - Цю модифікацію не можна видалити або оновити, оскільки вона необхідна для запуску наступних модифікацій + Цю модифікацію не можна видалити або оновити, оскільки вона необхідна для запуску наступних модифікацій - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Це вкладена модифікація, і її не можна встановити або видалити окремо від батьківської модифікації - + Notes Примітки - + All supported files Усі підтримувані файли - + Maps Мапи - + Campaigns Кампанії - + Configs Налаштування - + Mods Модифікації - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? Замінити файл налаштувань? - + Do you want to replace %1? Ви дійсно хочете замінити %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Завантажується %1. %p% (%v MB з %m MB) завершено - + Download failed Помилка завантаження - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Encountered errors: - + Install successfully downloaded? @@ -497,39 +486,39 @@ Install successfully downloaded? Встановити успішно завантажені? - + Installing chronicles - + Installing mod %1 Встановлення модифікації %1 - + Operation failed Операція завершилася невдало - + Encountered errors: Виникли помилки: - + screenshots знімки екрану - + Screenshot %1 Знімок екрану %1 - + Mod is incompatible Модифікація несумісна @@ -537,97 +526,77 @@ Install successfully downloaded? CModManager - Can not install submod - Неможливо встановити вкладену модифікацію + Неможливо встановити вкладену модифікацію - Mod is already installed - Модифікація вже встановлена + Модифікація вже встановлена - Can not uninstall submod - Неможливо видалити вкладену модифікацію + Неможливо видалити вкладену модифікацію - Mod is not installed - Модифікація не встановлена + Модифікація не встановлена - Mod is already enabled - Модифікація вже увімкнена + Модифікація вже увімкнена - - Mod must be installed first - Спочатку потрібно встановити модифікацію + Спочатку потрібно встановити модифікацію - Mod is not compatible, please update VCMI and checkout latest mod revisions - Модифікація несумісна, будь ласка, оновіть VCMI та перевірте останні версії модифікацій + Модифікація несумісна, будь ласка, оновіть VCMI та перевірте останні версії модифікацій - Required mod %1 is missing - Необхідна модифікація %1 відсутня + Необхідна модифікація %1 відсутня - Required mod %1 is not enabled - Необхідну модифікацію %1 не ввімкнено + Необхідну модифікацію %1 не ввімкнено - - This mod conflicts with %1 - Ця модифікація несумісна з %1 + Ця модифікація несумісна з %1 - Mod is already disabled - Модифікацію вже вимкнено + Модифікацію вже вимкнено - This mod is needed to run %1 - Ця модифікація необхідна для запуску %1 + Ця модифікація необхідна для запуску %1 - Mod archive is missing - Архів з модифікацією відсутній + Архів з модифікацією відсутній - Mod with such name is already installed - Модифікацію з такою назвою вже встановлено + Модифікацію з такою назвою вже встановлено - Mod archive is invalid or corrupted - Архів модифікації непридатний або пошкоджений + Архів модифікації непридатний або пошкоджений - Failed to extract mod data - Не вдалося видобути дані модифікації + Не вдалося видобути дані модифікації - Data with this mod was not found - Дані з цією модифікацією не знайдено + Дані з цією модифікацією не знайдено - Mod is located in protected directory, please remove it manually: - Модифікація знаходиться в захищеному каталозі, будь ласка, видаліть її вручну: + Модифікація знаходиться в захищеному каталозі, будь ласка, видаліть її вручну: @@ -1093,29 +1062,26 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use File size - %1 B - %1 Б + %1 Б - %1 KiB - %1 КіБ + %1 КіБ - + + %1 MiB %1 МіБ - %1 GiB - %1 ГіБ + %1 ГіБ - %1 TiB - %1 ТіБ + %1 ТіБ @@ -1565,16 +1531,209 @@ error reason: ModFields - + Name Назва - + Type Тип + + ModStateController + + + Can not install submod + Неможливо встановити вкладену модифікацію + + + + Mod is already installed + Модифікація вже встановлена + + + + Can not uninstall submod + Неможливо видалити вкладену модифікацію + + + + Mod is not installed + Модифікація не встановлена + + + + Mod is already enabled + Модифікація вже увімкнена + + + + + Mod must be installed first + Спочатку потрібно встановити модифікацію + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Модифікація несумісна, будь ласка, оновіть VCMI та перевірте останні версії модифікацій + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Необхідна модифікація %1 відсутня + + + + Mod is already disabled + Модифікацію вже вимкнено + + + + Mod archive is missing + Архів з модифікацією відсутній + + + + Mod with such name is already installed + Модифікацію з такою назвою вже встановлено + + + + Mod archive is invalid or corrupted + Архів модифікації непридатний або пошкоджений + + + + Failed to extract mod data + Не вдалося видобути дані модифікації + + + + Data with this mod was not found + Дані з цією модифікацією не знайдено + + + + Mod is located in protected directory, please remove it manually: + + Модифікація знаходиться в захищеному каталозі, будь ласка, видаліть її вручну: + + + + + ModStateItemModel + + + Translation + Переклад + + + + Town + Місто + + + + Test + Тестування + + + + Templates + Шаблони + + + + Spells + Закляття + + + + Music + Музика + + + + Maps + Мапи + + + + Sounds + Звуки + + + + Skills + Вміння + + + + + Other + Інше + + + + Objects + Об'єкти + + + + + Mechanics + Механіки + + + + + Interface + Інтерфейс + + + + Heroes + Герої + + + + + Graphical + Графічний + + + + Expansion + Розширення + + + + Creatures + Істоти + + + + Compatibility + Сумісність + + + + Artifacts + Артефакти + + + + AI + ШІ + + QObject diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts index edaae3fa7..8a12ddb56 100644 --- a/launcher/translation/vietnamese.ts +++ b/launcher/translation/vietnamese.ts @@ -90,108 +90,80 @@ CModListModel - Translation - Bản dịch + Bản dịch - Town - Thành phố + Thành phố - Test - Kiểm tra + Kiểm tra - Templates - Mẫu + Mẫu - Spells - Phép + Phép - Music - Nhạc + Nhạc - - Maps - - - - Sounds - Âm thanh + Âm thanh - Skills - Kĩ năng + Kĩ năng - - Other - Khác + Khác - Objects - Đối tượng + Đối tượng - - Mechanics - Cơ chế + Cơ chế - - Interface - Giao diện + Giao diện - Heroes - Tướng + Tướng - - Graphical - Đồ họa + Đồ họa - Expansion - Bản mở rộng + Bản mở rộng - Creatures - Quái + Quái - Compatibility - Tương thích + Tương thích - Artifacts - Vật phẩm + Vật phẩm - AI - Trí tuệ nhân tạo + Trí tuệ nhân tạo @@ -233,7 +205,7 @@ - + Description Mô tả @@ -293,189 +265,202 @@ Hủy - + Mod name Tên bản sửa đổi - + + Installed version Phiên bản cài đặt - + + Latest version Phiên bản mới nhất - + Size - + Download size Kích thước tải về - + Authors Tác giả - + License Giấy phép - + Contact Liên hệ - + Compatibility Tương thích - - + + Required VCMI version Cần phiên bản VCMI - + Supported VCMI version Hỗ trợ phiên bản VCMI - + please upgrade mod - - + + mods repository index - + or newer - + Supported VCMI versions Phiên bản VCMI hỗ trợ - + Languages Ngôn ngữ - + Required mods Cần các bản sửa đổi - + Conflicting mods Bản sửa đổi không tương thích - This mod can not be installed or enabled because the following dependencies are not present - Bản sửa đổi này không thể cài đặt hoặc kích hoạt do thiếu các bản sửa đổi sau + Bản sửa đổi này không thể cài đặt hoặc kích hoạt do thiếu các bản sửa đổi sau - This mod can not be enabled because the following mods are incompatible with it - Bản sửa đổi này không thể kích hoạt do không tương thích các bản sửa đổi sau + Bản sửa đổi này không thể kích hoạt do không tương thích các bản sửa đổi sau - This mod cannot be disabled because it is required by the following mods - Bản sửa đổi này không thể tắt do cần thiết cho các bản sửa đổi sau + Bản sửa đổi này không thể tắt do cần thiết cho các bản sửa đổi sau - This mod cannot be uninstalled or updated because it is required by the following mods - Bản sửa đổi này không thể gỡ bỏ hoặc nâng cấp do cần thiết cho các bản sửa đổi sau + Bản sửa đổi này không thể gỡ bỏ hoặc nâng cấp do cần thiết cho các bản sửa đổi sau - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Đây là bản con, không thể cài đặt hoặc gỡ bỏ tách biệt với bản cha - + Notes Ghi chú - + All supported files - + Maps - + Campaigns - + Configs - + Mods Bản sửa đổi - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? - + Do you want to replace %1? - + Downloading %1. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -484,145 +469,49 @@ Encountered errors: - + Install successfully downloaded? - + Installing chronicles - + Installing mod %1 - + Operation failed - + Encountered errors: - + screenshots - + Screenshot %1 Hình ảnh %1 - + Mod is incompatible Bản sửa đổi này không tương thích - - CModManager - - - Can not install submod - - - - - Mod is already installed - - - - - Can not uninstall submod - - - - - Mod is not installed - - - - - Mod is already enabled - - - - - - Mod must be installed first - - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - - - - - Required mod %1 is missing - - - - - Required mod %1 is not enabled - - - - - - This mod conflicts with %1 - - - - - Mod is already disabled - - - - - This mod is needed to run %1 - - - - - Mod archive is missing - - - - - Mod with such name is already installed - - - - - Mod archive is invalid or corrupted - - - - - Failed to extract mod data - - - - - Data with this mod was not found - - - - - Mod is located in protected directory, please remove it manually: - - - - CSettingsView @@ -1085,30 +974,11 @@ Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng đ File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1552,16 +1422,208 @@ error reason: ModFields - + Name Tên - + Type Loại + + ModStateController + + + Can not install submod + + + + + Mod is already installed + + + + + Can not uninstall submod + + + + + Mod is not installed + + + + + Mod is already enabled + + + + + + Mod must be installed first + + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + + + + + Mod is already disabled + + + + + Mod archive is missing + + + + + Mod with such name is already installed + + + + + Mod archive is invalid or corrupted + + + + + Failed to extract mod data + + + + + Data with this mod was not found + + + + + Mod is located in protected directory, please remove it manually: + + + + + + ModStateItemModel + + + Translation + Bản dịch + + + + Town + Thành phố + + + + Test + Kiểm tra + + + + Templates + Mẫu + + + + Spells + Phép + + + + Music + Nhạc + + + + Maps + + + + + Sounds + Âm thanh + + + + Skills + Kĩ năng + + + + + Other + Khác + + + + Objects + Đối tượng + + + + + Mechanics + Cơ chế + + + + + Interface + Giao diện + + + + Heroes + Tướng + + + + + Graphical + Đồ họa + + + + Expansion + Bản mở rộng + + + + Creatures + Quái + + + + Compatibility + Tương thích + + + + Artifacts + Vật phẩm + + + + AI + Trí tuệ nhân tạo + + QObject diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index d96f4e79d..46fa481cf 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -361,6 +361,14 @@ ui64 CCreatureSet::getArmyStrength() const return ret; } +ui64 CCreatureSet::getArmyCost() const +{ + ui64 ret = 0; + for (const auto& elem : stacks) + ret += elem.second->getMarketValue(); + return ret; +} + ui64 CCreatureSet::getPower(const SlotID & slot) const { return getStack(slot).getPower(); @@ -725,7 +733,7 @@ int CStackInstance::getExpRank() const int CStackInstance::getLevel() const { - return std::max(1, static_cast(getType()->getLevel())); + return std::max(1, getType()->getLevel()); } void CStackInstance::giveStackExp(TExpType exp) @@ -853,6 +861,12 @@ ui64 CStackInstance::getPower() const return static_cast(getType()->getAIValue()) * count; } +ui64 CStackInstance::getMarketValue() const +{ + assert(getType()); + return getType()->getFullRecruitCost().marketValue() * count; +} + ArtBearer::ArtBearer CStackInstance::bearerType() const { return ArtBearer::CREATURE; diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index 97065b597..c671bd547 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -107,6 +107,8 @@ public: FactionID getFactionID() const override; virtual ui64 getPower() const; + /// Returns total market value of resources needed to recruit this unit + virtual ui64 getMarketValue() const; CCreature::CreatureQuantityId getQuantityID() const; std::string getQuantityTXT(bool capitalized = true) const; virtual int getExpRank() const; @@ -272,6 +274,7 @@ public: int stacksCount() const; virtual bool needsLastStack() const; //true if last stack cannot be taken ui64 getArmyStrength() const; //sum of AI values of creatures + ui64 getArmyCost() const; //sum of cost of creatures ui64 getPower(const SlotID & slot) const; //value of specific stack std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack std::string getArmyDescription() const; diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index aca9b5919..85e1c95a8 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -157,10 +157,11 @@ set(lib_MAIN_SRCS modding/ActiveModsInSaveList.cpp modding/CModHandler.cpp - modding/CModInfo.cpp modding/CModVersion.cpp modding/ContentTypeHandler.cpp modding/IdentifierStorage.cpp + modding/ModDescription.cpp + modding/ModManager.cpp modding/ModUtility.cpp modding/ModVerificationInfo.cpp @@ -547,11 +548,12 @@ set(lib_MAIN_HEADERS modding/ActiveModsInSaveList.h modding/CModHandler.h - modding/CModInfo.h modding/CModVersion.h modding/ContentTypeHandler.h modding/IdentifierStorage.h + modding/ModDescription.h modding/ModIncompatibility.h + modding/ModManager.h modding/ModScope.h modding/ModUtility.h modding/ModVerificationInfo.h diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 8207ca558..e5436ba08 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -32,7 +32,9 @@ CSkill::CSkill(const SecondarySkill & id, std::string identifier, bool obligator id(id), identifier(std::move(identifier)), obligatoryMajor(obligatoryMajor), - obligatoryMinor(obligatoryMinor) + obligatoryMinor(obligatoryMinor), + special(false), + onlyOnWaterMap(false) { gainChance[0] = gainChance[1] = 0; //affects CHeroClassHandler::afterLoadFinalization() levels.resize(NSecondarySkill::levels.size() - 1); @@ -216,6 +218,7 @@ std::shared_ptr CSkillHandler::loadFromJson(const std::string & scope, c skill->modScope = scope; skill->onlyOnWaterMap = json["onlyOnWaterMap"].Bool(); + skill->special = json["special"].Bool(); VLC->generaltexth->registerString(scope, skill->getNameTextID(), json["name"]); switch(json["gainChance"].getType()) @@ -275,7 +278,8 @@ std::set CSkillHandler::getDefaultAllowed() const std::set result; for (auto const & skill : objects) - result.insert(skill->getId()); + if (!skill->special) + result.insert(skill->getId()); return result; } diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index e54797268..ae89435eb 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -75,6 +75,7 @@ public: void serializeJson(JsonSerializeFormat & handler); bool onlyOnWaterMap; + bool special; friend class CSkillHandler; friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill); diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 563520cb6..2e177d8ef 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -89,6 +89,7 @@ const std::vector GameSettings::settingProperties = {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, + {EGameSettings::RESOURCES_WEEKLY_BONUSES_AI, "resources", "weeklyBonusesAI" }, {EGameSettings::TEXTS_ARTIFACT, "textData", "artifact" }, {EGameSettings::TEXTS_CREATURE, "textData", "creature" }, {EGameSettings::TEXTS_FACTION, "textData", "faction" }, diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 6c7bf82c4..7cfef68cf 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -41,7 +41,6 @@ #include "gameState/QuestInfo.h" #include "mapping/CMap.h" #include "modding/CModHandler.h" -#include "modding/CModInfo.h" #include "modding/IdentifierStorage.h" #include "modding/CModVersion.h" #include "modding/ActiveModsInSaveList.h" diff --git a/lib/IGameSettings.h b/lib/IGameSettings.h index 9922294af..0fcae51f5 100644 --- a/lib/IGameSettings.h +++ b/lib/IGameSettings.h @@ -67,6 +67,7 @@ enum class EGameSettings PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, PATHFINDER_USE_MONOLITH_TWO_WAY, PATHFINDER_USE_WHIRLPOOL, + RESOURCES_WEEKLY_BONUSES_AI, TEXTS_ARTIFACT, TEXTS_CREATURE, TEXTS_FACTION, diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index cc5cd7a64..9c33db5fc 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -148,6 +148,26 @@ public: return ret; } + //Returns how many items of "this" we can afford with provided funds + int maxPurchasableCount(const ResourceSet& availableFunds) { + int ret = 0; // Initialize to 0 because we want the maximum number of accumulations + + for (size_t i = 0; i < container.size(); ++i) { + if (container.at(i) > 0) { // We only care about fulfilling positive needs + if (availableFunds[i] == 0) { + // If income is 0 and we need a positive amount, it's impossible to fulfill + return INT_MAX; + } + else { + // Calculate the number of times we need to accumulate income to fulfill the need + int ceiledResult = vstd::divideAndCeil(container.at(i), availableFunds[i]); + ret = std::max(ret, ceiledResult); + } + } + } + return ret; + } + ResourceSet & operator=(const TResource &rhs) { for(int & i : container) @@ -169,17 +189,6 @@ public: return this->container == rhs.container; } -// WARNING: comparison operators are used for "can afford" relation: a <= b means that foreach i a[i] <= b[i] -// that doesn't work the other way: a > b doesn't mean that a cannot be afforded with b, it's still b can afford a -// bool operator<(const ResourceSet &rhs) -// { -// for(int i = 0; i < size(); i++) -// if(at(i) >= rhs[i]) -// return false; -// -// return true; -// } - template void serialize(Handler &h) { h & container; diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 91d3da34b..b53dc06af 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -26,7 +26,6 @@ #include "entities/hero/CHeroHandler.h" #include "texts/CGeneralTextHandler.h" #include "modding/CModHandler.h" -#include "modding/CModInfo.h" #include "modding/IdentifierStorage.h" #include "modding/CModVersion.h" #include "IGameEventsReceiver.h" @@ -157,55 +156,44 @@ void LibClasses::loadModFilesystem() CStopWatch loadTime; modh = std::make_unique(); identifiersHandler = std::make_unique(); - modh->loadMods(); logGlobal->info("\tMod handler: %d ms", loadTime.getDiff()); modh->loadModFilesystems(); logGlobal->info("\tMod filesystems: %d ms", loadTime.getDiff()); } -static void logHandlerLoaded(const std::string & name, CStopWatch & timer) -{ - logGlobal->info("\t\t %s handler: %d ms", name, timer.getDiff()); -} - -template void createHandler(std::shared_ptr & handler, const std::string &name, CStopWatch &timer) +template void createHandler(std::shared_ptr & handler) { handler = std::make_shared(); - logHandlerLoaded(name, timer); } void LibClasses::init(bool onlyEssential) { - CStopWatch pomtime; - CStopWatch totalTime; - - createHandler(settingsHandler, "Game Settings", pomtime); + createHandler(settingsHandler); modh->initializeConfig(); - createHandler(generaltexth, "General text", pomtime); - createHandler(bth, "Bonus type", pomtime); - createHandler(roadTypeHandler, "Road", pomtime); - createHandler(riverTypeHandler, "River", pomtime); - createHandler(terrainTypeHandler, "Terrain", pomtime); - createHandler(heroh, "Hero", pomtime); - createHandler(heroclassesh, "Hero classes", pomtime); - createHandler(arth, "Artifact", pomtime); - createHandler(creh, "Creature", pomtime); - createHandler(townh, "Town", pomtime); - createHandler(biomeHandler, "Obstacle set", pomtime); - createHandler(objh, "Object", pomtime); - createHandler(objtypeh, "Object types information", pomtime); - createHandler(spellh, "Spell", pomtime); - createHandler(skillh, "Skill", pomtime); - createHandler(terviewh, "Terrain view pattern", pomtime); - createHandler(tplh, "Template", pomtime); //templates need already resolved identifiers (refactor?) + createHandler(generaltexth); + createHandler(bth); + createHandler(roadTypeHandler); + createHandler(riverTypeHandler); + createHandler(terrainTypeHandler); + createHandler(heroh); + createHandler(heroclassesh); + createHandler(arth); + createHandler(creh); + createHandler(townh); + createHandler(biomeHandler); + createHandler(objh); + createHandler(objtypeh); + createHandler(spellh); + createHandler(skillh); + createHandler(terviewh); + createHandler(tplh); //templates need already resolved identifiers (refactor?) #if SCRIPTING_ENABLED - createHandler(scriptHandler, "Script", pomtime); + createHandler(scriptHandler); #endif - createHandler(battlefieldsHandler, "Battlefields", pomtime); - createHandler(obstacleHandler, "Obstacles", pomtime); - logGlobal->info("\tInitializing handlers: %d ms", totalTime.getDiff()); + createHandler(battlefieldsHandler); + createHandler(obstacleHandler); modh->load(); modh->afterLoad(onlyEssential); diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index d4ec621c6..251c1f9b7 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1424,7 +1424,7 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes( { //friendly stacks can also be damaged by Dragon Breath const auto * st = battleGetUnitByPos(nextHex, true); - if(st != nullptr) + if(st != nullptr && st != attacker) //but not unit itself (doublewide + prism attack) at.friendlyCreaturePositions.insert(nextHex); } } diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index 89d4eed8c..5dc6a2e10 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -339,7 +339,7 @@ void CampaignState::setCurrentMapAsConquered(std::vector heroe { range::sort(heroes, [](const CGHeroInstance * a, const CGHeroInstance * b) { - return a->getHeroStrength() > b->getHeroStrength(); + return a->getHeroStrengthForCampaign() > b->getHeroStrengthForCampaign(); }); logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated()); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index c4a1f6272..3bc547247 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -298,7 +298,7 @@ public: HORDE_2_UPGR, GRAIL, EXTRA_TOWN_HALL, EXTRA_CITY_HALL, EXTRA_CAPITOL, DWELL_FIRST=30, DWELL_LVL_2, DWELL_LVL_3, DWELL_LVL_4, DWELL_LVL_5, DWELL_LVL_6, DWELL_LAST=36, DWELL_UP_FIRST=37, DWELL_LVL_2_UP, DWELL_LVL_3_UP, DWELL_LVL_4_UP, DWELL_LVL_5_UP, - DWELL_LVL_6_UP, DWELL_UP_LAST=43, DWELL_LVL_8=150, DWELL_LVL_8_UP=151, + DWELL_LVL_6_UP, DWELL_UP_LAST=43, DWELL_LVL_8=150, DWELL_LVL_8_UP=151, //150-154 reserved for 8. creature with potential upgrades DWELL_LVL_1 = DWELL_FIRST, DWELL_LVL_7 = DWELL_LAST, @@ -337,7 +337,10 @@ public: if (it != tmp.end()) return std::distance(tmp.begin(), it); } - return (dwelling - DWELL_FIRST) % (GameConstants::CREATURES_PER_TOWN - 1); + if(dwelling >= BuildingIDBase::DWELL_LVL_8 && dwelling < BuildingIDBase::DWELL_LVL_8 + 5) + return 7; + else + return (dwelling - DWELL_FIRST) % (GameConstants::CREATURES_PER_TOWN - 1); } static int getUpgradedFromDwelling(BuildingIDBase dwelling) @@ -349,15 +352,18 @@ public: if (it != tmp.end()) return i; } - return (dwelling - DWELL_FIRST) / (GameConstants::CREATURES_PER_TOWN - 1); + if(dwelling >= BuildingIDBase::DWELL_LVL_8 && dwelling < BuildingIDBase::DWELL_LVL_8 + 5) + return dwelling - BuildingIDBase::DWELL_LVL_8; + else + return (dwelling - DWELL_FIRST) / (GameConstants::CREATURES_PER_TOWN - 1); } static void advanceDwelling(BuildingIDBase & dwelling) { - if(dwelling != BuildingIDBase::DWELL_LVL_8) - dwelling.advance(GameConstants::CREATURES_PER_TOWN - 1); - else + if(dwelling >= BuildingIDBase::DWELL_LVL_8 && dwelling < BuildingIDBase::DWELL_LVL_8 + 5) dwelling.advance(1); + else + dwelling.advance(GameConstants::CREATURES_PER_TOWN - 1); } bool IsSpecialOrGrail() const diff --git a/lib/entities/hero/CHeroClass.cpp b/lib/entities/hero/CHeroClass.cpp index 4a8d6d308..80e06cabf 100644 --- a/lib/entities/hero/CHeroClass.cpp +++ b/lib/entities/hero/CHeroClass.cpp @@ -30,7 +30,10 @@ SecondarySkill CHeroClass::chooseSecSkill(const std::set & possi { skills.push_back(possible); if (secSkillProbability.count(possible) != 0) - weights.push_back(secSkillProbability.at(possible)); + { + int weight = secSkillProbability.at(possible); + weights.push_back(std::max(1, weight)); + } else weights.push_back(1); // H3 behavior - banned skills have minimal (1) chance to be picked } diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index e3df56e34..4840b3f87 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -212,6 +212,7 @@ ISimpleResourceLoader * CResourceHandler::get() ISimpleResourceLoader * CResourceHandler::get(const std::string & identifier) { + assert(knownLoaders.count(identifier)); return knownLoaders.at(identifier); } diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 91a58b796..7df3c63e9 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -62,7 +62,7 @@ boost::shared_mutex CGameState::mutex; HeroTypeID CGameState::pickNextHeroType(const PlayerColor & owner) { const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner); - if(ps.hero >= HeroTypeID(0) && !isUsedHero(HeroTypeID(ps.hero))) //we haven't used selected hero + if(ps.hero.isValid() && !isUsedHero(HeroTypeID(ps.hero))) //we haven't used selected hero { return HeroTypeID(ps.hero); } diff --git a/lib/json/JsonNode.cpp b/lib/json/JsonNode.cpp index 3fd979e00..9df1fb38d 100644 --- a/lib/json/JsonNode.cpp +++ b/lib/json/JsonNode.cpp @@ -86,6 +86,11 @@ JsonNode::JsonNode(const std::string & string) { } +JsonNode::JsonNode(const JsonMap & map) + : data(map) +{ +} + JsonNode::JsonNode(const std::byte * data, size_t datasize, const std::string & fileName) : JsonNode(data, datasize, JsonParsingSettings(), fileName) { diff --git a/lib/json/JsonNode.h b/lib/json/JsonNode.h index 7ac55f57e..9ccb13cba 100644 --- a/lib/json/JsonNode.h +++ b/lib/json/JsonNode.h @@ -71,6 +71,9 @@ public: explicit JsonNode(const char * string); explicit JsonNode(const std::string & string); + /// Create tree from map + explicit JsonNode(const JsonMap & map); + /// Create tree from Json-formatted input explicit JsonNode(const std::byte * data, size_t datasize, const std::string & fileName); explicit JsonNode(const std::byte * data, size_t datasize, const JsonParsingSettings & parserSettings, const std::string & fileName); @@ -187,7 +190,7 @@ void convert(std::map & value, const JsonNode & node) { value.clear(); for(const JsonMap::value_type & entry : node.Struct()) - value.insert(entry.first, entry.second.convertTo()); + value.emplace(entry.first, entry.second.convertTo()); } template diff --git a/lib/json/JsonValidator.cpp b/lib/json/JsonValidator.cpp index 3cb89846f..2dde7b338 100644 --- a/lib/json/JsonValidator.cpp +++ b/lib/json/JsonValidator.cpp @@ -422,7 +422,7 @@ static std::string requiredCheck(JsonValidator & validator, const JsonNode & bas std::string errors; for(const auto & required : schema.Vector()) { - if (data[required.String()].isNull()) + if (data[required.String()].isNull() && data.getModScope() != "core") errors += validator.makeErrorMessage("Required entry " + required.String() + " is missing"); } return errors; diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index e8356d7ef..422c8e2cf 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -523,7 +523,7 @@ void CObjectClassesHandler::afterLoadFinalization() obj->afterLoadFinalization(); if(obj->getTemplates().empty()) - logGlobal->warn("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey()); + logMod->debug("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey()); } } diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 57b52bd43..a3986304a 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -17,8 +17,10 @@ #include "../TerrainHandler.h" #include "../VCMI_Lib.h" +#include "../CConfigHandler.h" #include "../entities/faction/CTownHandler.h" #include "../entities/hero/CHeroClass.h" +#include "../json/JsonUtils.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGMarket.h" #include "../mapObjects/CGTownInstance.h" @@ -242,10 +244,28 @@ AnimationPath BoatInstanceConstructor::getBoatAnimationName() const void MarketInstanceConstructor::initTypeData(const JsonNode & input) { + if (settings["mods"]["validation"].String() != "off") + JsonUtils::validate(input, "vcmi:market", getJsonKey()); + if (!input["description"].isNull()) { - description = input["description"].String(); - VLC->generaltexth->registerString(input.getModScope(), TextIdentifier(getBaseTextID(), "description"), description); + std::string description = input["description"].String(); + descriptionTextID = TextIdentifier(getBaseTextID(), "description").get(); + VLC->generaltexth->registerString( input.getModScope(), descriptionTextID, input["description"]); + } + + if (!input["speech"].isNull()) + { + std::string speech = input["speech"].String(); + if (!speech.empty() && speech.at(0) == '@') + { + speechTextID = speech.substr(1); + } + else + { + speechTextID = TextIdentifier(getBaseTextID(), "speech").get(); + VLC->generaltexth->registerString( input.getModScope(), speechTextID, input["speech"]); + } } for(auto & element : input["modes"].Vector()) @@ -256,14 +276,11 @@ void MarketInstanceConstructor::initTypeData(const JsonNode & input) marketEfficiency = input["efficiency"].isNull() ? 5 : input["efficiency"].Integer(); predefinedOffer = input["offer"]; - - title = input["title"].String(); - speech = input["speech"].String(); } bool MarketInstanceConstructor::hasDescription() const { - return !description.empty(); + return !descriptionTextID.empty(); } CGMarket * MarketInstanceConstructor::createObject(IGameCallback * cb) const @@ -283,21 +300,6 @@ CGMarket * MarketInstanceConstructor::createObject(IGameCallback * cb) const return new CGMarket(cb); } -void MarketInstanceConstructor::initializeObject(CGMarket * market) const -{ - market->marketEfficiency = marketEfficiency; - - if(auto university = dynamic_cast(market)) - { - university->title = market->getObjectName(); - if(!title.empty()) - university->title = VLC->generaltexth->translate(title); - - if(!speech.empty()) - university->speech = VLC->generaltexth->translate(speech); - } -} - const std::set & MarketInstanceConstructor::availableModes() const { return marketModes; @@ -315,4 +317,15 @@ void MarketInstanceConstructor::randomizeObject(CGMarket * object, vstd::RNG & r } } +std::string MarketInstanceConstructor::getSpeechTranslated() const +{ + assert(marketModes.count(EMarketMode::RESOURCE_SKILL)); + return VLC->generaltexth->translate(speechTextID); +} + +int MarketInstanceConstructor::getMarketEfficiency() const +{ + return marketEfficiency; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index 8d4abcc67..73ba43874 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -115,25 +115,23 @@ public: class MarketInstanceConstructor : public CDefaultObjectTypeHandler { -protected: - void initTypeData(const JsonNode & config) override; + std::string descriptionTextID; + std::string speechTextID; std::set marketModes; JsonNode predefinedOffer; int marketEfficiency; - - std::string description; - std::string title; - std::string speech; - + + void initTypeData(const JsonNode & config) override; public: CGMarket * createObject(IGameCallback * cb) const override; - void initializeObject(CGMarket * object) const override; void randomizeObject(CGMarket * object, vstd::RNG & rng) const override; const std::set & availableModes() const; bool hasDescription() const; + std::string getSpeechTranslated() const; + int getMarketEfficiency() const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/HillFortInstanceConstructor.cpp b/lib/mapObjectConstructors/HillFortInstanceConstructor.cpp index f71f4990e..889868b0a 100644 --- a/lib/mapObjectConstructors/HillFortInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/HillFortInstanceConstructor.cpp @@ -18,7 +18,9 @@ VCMI_LIB_NAMESPACE_BEGIN void HillFortInstanceConstructor::initTypeData(const JsonNode & config) { parameters = config; - VLC->generaltexth->registerString(parameters.getModScope(), TextIdentifier(getBaseTextID(), "unavailableUpgradeMessage"), parameters["unavailableUpgradeMessage"].String()); + if(!parameters["unavailableUpgradeMessage"].isNull()) + VLC->generaltexth->registerString(parameters.getModScope(), TextIdentifier(getBaseTextID(), "unavailableUpgradeMessage"), parameters["unavailableUpgradeMessage"].String()); + VLC->generaltexth->registerString(parameters.getModScope(), TextIdentifier(getBaseTextID(), "description"), parameters["description"].String()); } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 7380ef70b..6e84d0baf 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -661,8 +661,10 @@ void CGHeroInstance::pickRandomObject(vstd::RNG & rand) if (ID == Obj::RANDOM_HERO) { + auto selectedHero = cb->gameState()->pickNextHeroType(getOwner()); + ID = Obj::HERO; - subID = cb->gameState()->pickNextHeroType(getOwner()); + subID = selectedHero; randomizeArmy(getHeroClass()->faction); } @@ -709,7 +711,25 @@ double CGHeroInstance::getFightingStrength() const double CGHeroInstance::getMagicStrength() const { - return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER))); + if (!hasSpellbook()) + return 1; + bool atLeastOneCombatSpell = false; + for (auto spell : spells) + { + if (spellbookContainsSpell(spell) && spell.toSpell()->isCombat()) + { + atLeastOneCombatSpell = true; + break; + } + } + if (!atLeastOneCombatSpell) + return 1; + return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE) * mana / manaLimit()) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER) * mana / manaLimit())); +} + +double CGHeroInstance::getMagicStrengthForCampaign() const +{ + return sqrt((1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::SPELL_POWER))); } double CGHeroInstance::getHeroStrength() const @@ -717,9 +737,14 @@ double CGHeroInstance::getHeroStrength() const return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrength(), 2.0)); } +double CGHeroInstance::getHeroStrengthForCampaign() const +{ + return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrengthForCampaign(), 2.0)); +} + ui64 CGHeroInstance::getTotalStrength() const { - double ret = getFightingStrength() * getArmyStrength(); + double ret = getHeroStrength() * getArmyStrength(); return static_cast(ret); } @@ -1894,5 +1919,11 @@ const IOwnableObject * CGHeroInstance::asOwnable() const return this; } +int CGHeroInstance::getBasePrimarySkillValue(PrimarySkill which) const +{ + std::string cachingStr = "type_PRIMARY_SKILL_base_" + std::to_string(static_cast(which)); + auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)); + return valOfBonuses(selector, cachingStr); +} VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index c673b1727..e453760b8 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -223,10 +223,13 @@ public: int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false, const TurnInfo * ti = nullptr) const; double getFightingStrength() const; // takes attack / defense skill into account - double getMagicStrength() const; // takes knowledge / spell power skill into account + double getMagicStrength() const; // takes knowledge / spell power skill but also current mana, whether the hero owns a spell-book and whether that books contains anything into account + double getMagicStrengthForCampaign() const; // takes knowledge / spell power skill into account double getHeroStrength() const; // includes fighting and magic strength + double getHeroStrengthForCampaign() const; // includes fighting and the for-campaign-version of magic strength ui64 getTotalStrength() const; // includes fighting strength and army strength TExpType calculateXp(TExpType exp) const; //apply learning skill + int getBasePrimarySkillValue(PrimarySkill which) const; //the value of a base-skill without items or temporary bonuses CStackBasicDescriptor calculateNecromancy (const BattleResult &battleResult) const; void showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const; diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index ff2118bf2..a7eef18ad 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -57,7 +57,7 @@ std::string CGMarket::getPopupText(const CGHeroInstance * hero) const int CGMarket::getMarketEfficiency() const { - return marketEfficiency; + return getMarketHandler()->getMarketEfficiency(); } int CGMarket::availableUnits(EMarketMode mode, int marketItemSerial) const @@ -125,6 +125,11 @@ std::vector CGUniversity::availableItemsIds(EMarketMode mode) cons } } +std::string CGUniversity::getSpeechTranslated() const +{ + return getMarketHandler()->getSpeechTranslated(); +} + void CGUniversity::onHeroVisit(const CGHeroInstance * h) const { cb->showObjectWindow(this, EOpenWindowMode::UNIVERSITY_WINDOW, h, true); diff --git a/lib/mapObjects/CGMarket.h b/lib/mapObjects/CGMarket.h index b28a386bc..6f4f92a42 100644 --- a/lib/mapObjects/CGMarket.h +++ b/lib/mapObjects/CGMarket.h @@ -19,11 +19,10 @@ class MarketInstanceConstructor; class DLL_LINKAGE CGMarket : public CGObjectInstance, public IMarket { +protected: std::shared_ptr getMarketHandler() const; public: - int marketEfficiency; - CGMarket(IGameCallback *cb); ///IObjectInterface void onHeroVisit(const CGHeroInstance * h) const override; //open trading window @@ -48,7 +47,12 @@ public: h & marketModes; } - h & marketEfficiency; + if (h.version < Handler::Version::MARKET_TRANSLATION_FIX) + { + int unused = 0; + h & unused; + } + if (h.version < Handler::Version::NEW_MARKETS) { std::string speech; @@ -103,8 +107,8 @@ class DLL_LINKAGE CGUniversity : public CGMarket { public: using CGMarket::CGMarket; - std::string speech; //currently shown only in university - std::string title; + + std::string getSpeechTranslated() const; std::vector skills; //available skills @@ -115,10 +119,11 @@ public: { h & static_cast(*this); h & skills; - if (h.version >= Handler::Version::NEW_MARKETS) + if (h.version >= Handler::Version::NEW_MARKETS && h.version < Handler::Version::MARKET_TRANSLATION_FIX) { - h & speech; - h & title; + std::string temp; + h & temp; + h & temp; } } }; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index f314499db..037e98d22 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -378,6 +378,9 @@ void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const std::string CGTownInstance::getObjectName() const { + if(ID == Obj::RANDOM_TOWN ) + return CGObjectInstance::getObjectName(); + return getNameTranslated() + ", " + getTown()->faction->getNameTranslated(); } diff --git a/lib/mapObjects/FlaggableMapObject.cpp b/lib/mapObjects/FlaggableMapObject.cpp index 5b22650ae..56b64d1e1 100644 --- a/lib/mapObjects/FlaggableMapObject.cpp +++ b/lib/mapObjects/FlaggableMapObject.cpp @@ -51,6 +51,12 @@ void FlaggableMapObject::onHeroVisit( const CGHeroInstance * h ) const giveBonusTo(h->getOwner()); } +void FlaggableMapObject::markAsDeleted() const +{ + if(getOwner().isValidPlayer()) + takeBonusFrom(getOwner()); +} + void FlaggableMapObject::initObj(vstd::RNG & rand) { if(getOwner().isValidPlayer()) diff --git a/lib/mapObjects/FlaggableMapObject.h b/lib/mapObjects/FlaggableMapObject.h index f3c86e0d2..6a332320e 100644 --- a/lib/mapObjects/FlaggableMapObject.h +++ b/lib/mapObjects/FlaggableMapObject.h @@ -28,6 +28,7 @@ public: using CGObjectInstance::CGObjectInstance; void onHeroVisit(const CGHeroInstance * h) const override; + void markAsDeleted() const; void initObj(vstd::RNG & rand) override; const IOwnableObject * asOwnable() const final; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 78ab960ab..910baa639 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -1337,7 +1337,7 @@ std::string HillFort::getPopupText(PlayerColor player) const { MetaString message = MetaString::createFromRawString("{%s}\r\n\r\n%s"); - message.replaceName(ID); + message.replaceName(ID, subID); message.replaceTextID(getDescriptionToolTip()); return message.toString(); @@ -1356,6 +1356,7 @@ std::string HillFort::getDescriptionToolTip() const std::string HillFort::getUnavailableUpgradeMessage() const { + assert(getObjectHandler()->getModScope() != "core"); return TextIdentifier(getObjectHandler()->getBaseTextID(), "unavailableUpgradeMessage").get(); } diff --git a/lib/mapObjects/ObstacleSetHandler.cpp b/lib/mapObjects/ObstacleSetHandler.cpp index aef9461a8..4da7cac88 100644 --- a/lib/mapObjects/ObstacleSetHandler.cpp +++ b/lib/mapObjects/ObstacleSetHandler.cpp @@ -43,7 +43,7 @@ void ObstacleSet::removeEmptyTemplates() { if (tmpl->getBlockedOffsets().empty()) { - logMod->warn("Obstacle template %s blocks no tiles, removing it", tmpl->stringID); + logMod->debug("Obstacle template %s blocks no tiles, removing it", tmpl->stringID); return true; } return false; @@ -457,7 +457,7 @@ void ObstacleSetHandler::addTemplate(const std::string & scope, const std::strin if (VLC->identifiersHandler->getIdentifier(scope, "obstacleTemplate", strippedName, true)) { - logMod->warn("Duplicate obstacle template: %s", strippedName); + logMod->debug("Duplicate obstacle template: %s", strippedName); return; } else diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index db4650ce1..3760c3bc1 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -103,6 +103,9 @@ void CMapEvent::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("firstOccurrence", firstOccurrence); handler.serializeInt("nextOccurrence", nextOccurrence); resources.serializeJson(handler, "resources"); + + auto deletedObjects = handler.enterArray("deletedObjectsInstances"); + deletedObjects.serializeArray(deletedObjectsInstances); } void CCastleEvent::serializeJson(JsonSerializeFormat & handler) diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index b37fcc692..b8da3b405 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -12,6 +12,7 @@ #include "../ResourceSet.h" #include "../texts/MetaString.h" +#include "../int3.h" VCMI_LIB_NAMESPACE_BEGIN @@ -42,6 +43,8 @@ public: ui32 firstOccurrence; ui32 nextOccurrence; /// specifies after how many days the event will occur the next time; 0 if event occurs only one time + std::vector deletedObjectsInstances; + template void serialize(Handler & h) { @@ -64,6 +67,10 @@ public: h & computerAffected; h & firstOccurrence; h & nextOccurrence; + if(h.version >= Handler::Version::EVENT_OBJECTS_DELETION) + { + h & deletedObjectsInstances; + } } virtual void serializeJson(JsonSerializeFormat & handler); diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index 830245d12..61a1958b0 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -17,8 +17,8 @@ #include "../filesystem/CMemoryStream.h" #include "../filesystem/CMemoryBuffer.h" #include "../modding/CModHandler.h" +#include "../modding/ModDescription.h" #include "../modding/ModScope.h" -#include "../modding/CModInfo.h" #include "../VCMI_Lib.h" #include "CMap.h" @@ -99,7 +99,7 @@ ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) if(vstd::contains(activeMods, mapMod.first)) { const auto & modInfo = VLC->modh->getModInfo(mapMod.first); - if(modInfo.getVerificationInfo().version.compatible(mapMod.second.version)) + if(modInfo.getVersion().compatible(mapMod.second.version)) continue; } missingMods[mapMod.first] = mapMod.second; diff --git a/lib/modding/ActiveModsInSaveList.cpp b/lib/modding/ActiveModsInSaveList.cpp index 17e8927f0..c4620c170 100644 --- a/lib/modding/ActiveModsInSaveList.cpp +++ b/lib/modding/ActiveModsInSaveList.cpp @@ -11,7 +11,7 @@ #include "ActiveModsInSaveList.h" #include "../VCMI_Lib.h" -#include "CModInfo.h" +#include "ModDescription.h" #include "CModHandler.h" #include "ModIncompatibility.h" @@ -21,13 +21,13 @@ std::vector ActiveModsInSaveList::getActiveGameplayAffectingMods() { std::vector result; for (auto const & entry : VLC->modh->getActiveMods()) - if (VLC->modh->getModInfo(entry).checkModGameplayAffecting()) + if (VLC->modh->getModInfo(entry).affectsGameplay()) result.push_back(entry); return result; } -const ModVerificationInfo & ActiveModsInSaveList::getVerificationInfo(TModID mod) +ModVerificationInfo ActiveModsInSaveList::getVerificationInfo(TModID mod) { return VLC->modh->getModInfo(mod).getVerificationInfo(); } @@ -44,10 +44,10 @@ void ActiveModsInSaveList::verifyActiveMods(const std::mapmodh->getModInfo(compared.first).getVerificationInfo().name); + missingMods.push_back(VLC->modh->getModInfo(compared.first).getName()); if (compared.second == ModVerificationStatus::EXCESSIVE) - excessiveMods.push_back(VLC->modh->getModInfo(compared.first).getVerificationInfo().name); + excessiveMods.push_back(VLC->modh->getModInfo(compared.first).getName()); } if(!missingMods.empty() || !excessiveMods.empty()) diff --git a/lib/modding/ActiveModsInSaveList.h b/lib/modding/ActiveModsInSaveList.h index 0d51ed4f3..d89244788 100644 --- a/lib/modding/ActiveModsInSaveList.h +++ b/lib/modding/ActiveModsInSaveList.h @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class ActiveModsInSaveList { std::vector getActiveGameplayAffectingMods(); - const ModVerificationInfo & getVerificationInfo(TModID mod); + ModVerificationInfo getVerificationInfo(TModID mod); /// Checks whether provided mod list is compatible with current VLC and throws on failure void verifyActiveMods(const std::map & modList); @@ -29,7 +29,10 @@ public: std::vector activeMods = getActiveGameplayAffectingMods(); h & activeMods; for(const auto & m : activeMods) - h & getVerificationInfo(m); + { + ModVerificationInfo info = getVerificationInfo(m); + h & info; + } } else { diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index ab1f375ec..06518e107 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -10,297 +10,45 @@ #include "StdInc.h" #include "CModHandler.h" -#include "CModInfo.h" -#include "ModScope.h" #include "ContentTypeHandler.h" #include "IdentifierStorage.h" -#include "ModIncompatibility.h" +#include "ModDescription.h" +#include "ModManager.h" +#include "ModScope.h" -#include "../CCreatureHandler.h" #include "../CConfigHandler.h" -#include "../CStopWatch.h" +#include "../CCreatureHandler.h" #include "../GameSettings.h" #include "../ScriptHandler.h" -#include "../constants/StringConstants.h" +#include "../VCMI_Lib.h" #include "../filesystem/Filesystem.h" #include "../json/JsonUtils.h" -#include "../spells/CSpellHandler.h" #include "../texts/CGeneralTextHandler.h" #include "../texts/Languages.h" -#include "../VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN -static JsonNode loadModSettings(const JsonPath & path) -{ - if (CResourceHandler::get("local")->existsResource(ResourcePath(path))) - { - return JsonNode(path); - } - // Probably new install. Create initial configuration - CResourceHandler::get("local")->createResource(path.getOriginalName() + ".json"); - return JsonNode(); -} - CModHandler::CModHandler() : content(std::make_shared()) - , coreMod(std::make_unique()) + , modManager(std::make_unique()) { } CModHandler::~CModHandler() = default; -// currentList is passed by value to get current list of depending mods -bool CModHandler::hasCircularDependency(const TModID & modID, std::set currentList) const -{ - const CModInfo & mod = allMods.at(modID); - - // Mod already present? We found a loop - if (vstd::contains(currentList, modID)) - { - logMod->error("Error: Circular dependency detected! Printing dependency list:"); - logMod->error("\t%s -> ", mod.getVerificationInfo().name); - return true; - } - - currentList.insert(modID); - - // recursively check every dependency of this mod - for(const TModID & dependency : mod.dependencies) - { - if (hasCircularDependency(dependency, currentList)) - { - logMod->error("\t%s ->\n", mod.getVerificationInfo().name); // conflict detected, print dependency list - return true; - } - } - return false; -} - -// Returned vector affects the resource loaders call order (see CFilesystemList::load). -// The loaders call order matters when dependent mod overrides resources in its dependencies. -std::vector CModHandler::validateAndSortDependencies(std::vector modsToResolve) const -{ - // Topological sort algorithm. - // TODO: Investigate possible ways to improve performance. - boost::range::sort(modsToResolve); // Sort mods per name - std::vector sortedValidMods; // Vector keeps order of elements (LIFO) - sortedValidMods.reserve(modsToResolve.size()); // push_back calls won't cause memory reallocation - std::set resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements - std::set notResolvedModIDs(modsToResolve.begin(), modsToResolve.end()); // Use a set for validation for performance reason - - // Mod is resolved if it has no dependencies or all its dependencies are already resolved - auto isResolved = [&](const CModInfo & mod) -> bool - { - if(mod.dependencies.size() > resolvedModIDs.size()) - return false; - - for(const TModID & dependency : mod.dependencies) - { - if(!vstd::contains(resolvedModIDs, dependency)) - return false; - } - - for(const TModID & softDependency : mod.softDependencies) - { - if(vstd::contains(notResolvedModIDs, softDependency)) - return false; - } - - for(const TModID & conflict : mod.conflicts) - { - if(vstd::contains(resolvedModIDs, conflict)) - return false; - } - for(const TModID & reverseConflict : resolvedModIDs) - { - if (vstd::contains(allMods.at(reverseConflict).conflicts, mod.identifier)) - return false; - } - return true; - }; - - while(true) - { - std::set resolvedOnCurrentTreeLevel; - for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree - { - if(isResolved(allMods.at(*it))) - { - resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration - sortedValidMods.push_back(*it); - it = modsToResolve.erase(it); - continue; - } - it++; - } - if(!resolvedOnCurrentTreeLevel.empty()) - { - resolvedModIDs.insert(resolvedOnCurrentTreeLevel.begin(), resolvedOnCurrentTreeLevel.end()); - for(const auto & it : resolvedOnCurrentTreeLevel) - notResolvedModIDs.erase(it); - continue; - } - // If there are no valid mods on the current mods tree level, no more mod can be resolved, should be ended. - break; - } - - modLoadErrors = std::make_unique(); - - auto addErrorMessage = [this](const std::string & textID, const std::string & brokenModID, const std::string & missingModID) - { - modLoadErrors->appendTextID(textID); - - if (allMods.count(brokenModID)) - modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name); - else - modLoadErrors->replaceRawString(brokenModID); - - if (allMods.count(missingModID)) - modLoadErrors->replaceRawString(allMods.at(missingModID).getVerificationInfo().name); - else - modLoadErrors->replaceRawString(missingModID); - - }; - - // Left mods have unresolved dependencies, output all to log. - for(const auto & brokenModID : modsToResolve) - { - const CModInfo & brokenMod = allMods.at(brokenModID); - bool showErrorMessage = false; - for(const TModID & dependency : brokenMod.dependencies) - { - if(!vstd::contains(resolvedModIDs, dependency) && brokenMod.config["modType"].String() != "Compatibility") - { - addErrorMessage("vcmi.server.errors.modNoDependency", brokenModID, dependency); - showErrorMessage = true; - } - } - for(const TModID & conflict : brokenMod.conflicts) - { - if(vstd::contains(resolvedModIDs, conflict)) - { - addErrorMessage("vcmi.server.errors.modConflict", brokenModID, conflict); - showErrorMessage = true; - } - } - for(const TModID & reverseConflict : resolvedModIDs) - { - if (vstd::contains(allMods.at(reverseConflict).conflicts, brokenModID)) - { - addErrorMessage("vcmi.server.errors.modConflict", brokenModID, reverseConflict); - showErrorMessage = true; - } - } - - // some mods may in a (soft) dependency loop. - if(!showErrorMessage && brokenMod.config["modType"].String() != "Compatibility") - { - modLoadErrors->appendTextID("vcmi.server.errors.modDependencyLoop"); - if (allMods.count(brokenModID)) - modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name); - else - modLoadErrors->replaceRawString(brokenModID); - } - - } - return sortedValidMods; -} - -std::vector CModHandler::getModList(const std::string & path) const -{ - std::string modDir = boost::to_upper_copy(path + "MODS/"); - size_t depth = boost::range::count(modDir, '/'); - - auto list = CResourceHandler::get("initial")->getFilteredFiles([&](const ResourcePath & id) -> bool - { - if (id.getType() != EResType::DIRECTORY) - return false; - if (!boost::algorithm::starts_with(id.getName(), modDir)) - return false; - if (boost::range::count(id.getName(), '/') != depth ) - return false; - return true; - }); - - //storage for found mods - std::vector foundMods; - for(const auto & entry : list) - { - std::string name = entry.getName(); - name.erase(0, modDir.size()); //Remove path prefix - - if (!name.empty()) - foundMods.push_back(name); - } - return foundMods; -} - - - -void CModHandler::loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods) -{ - for(const std::string & modName : getModList(path)) - loadOneMod(modName, parent, modSettings, enableMods); -} - -void CModHandler::loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods) -{ - boost::to_lower(modName); - std::string modFullName = parent.empty() ? modName : parent + '.' + modName; - - if ( ModScope::isScopeReserved(modFullName)) - { - logMod->error("Can not load mod %s - this name is reserved for internal use!", modFullName); - return; - } - - if(CResourceHandler::get("initial")->existsResource(CModInfo::getModFile(modFullName))) - { - CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName))); - if (!parent.empty()) // this is submod, add parent to dependencies - mod.dependencies.insert(parent); - - allMods[modFullName] = mod; - if (mod.isEnabled() && enableMods) - activeMods.push_back(modFullName); - - loadMods(CModInfo::getModDir(modFullName) + '/', modFullName, modSettings[modName]["mods"], enableMods && mod.isEnabled()); - } -} - -void CModHandler::loadMods() -{ - JsonNode modConfig; - - modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json")); - loadMods("", "", modConfig["activeMods"], true); - - coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json"))); -} - std::vector CModHandler::getAllMods() const { - std::vector modlist; - modlist.reserve(allMods.size()); - for (auto & entry : allMods) - modlist.push_back(entry.first); - return modlist; + return modManager->getAllMods(); } -std::vector CModHandler::getActiveMods() const +const std::vector & CModHandler::getActiveMods() const { - return activeMods; + return modManager->getActiveMods(); } -std::string CModHandler::getModLoadErrors() const +const ModDescription & CModHandler::getModInfo(const TModID & modId) const { - return modLoadErrors->toString(); -} - -const CModInfo & CModHandler::getModInfo(const TModID & modId) const -{ - return allMods.at(modId); + return modManager->getModDescription(modId); } static JsonNode genDefaultFS() @@ -315,86 +63,67 @@ static JsonNode genDefaultFS() return defaultFS; } +static std::string getModDirectory(const TModID & modName) +{ + std::string result = modName; + boost::to_upper(result); + boost::algorithm::replace_all(result, ".", "/MODS/"); + return "MODS/" + result; +} + static ISimpleResourceLoader * genModFilesystem(const std::string & modName, const JsonNode & conf) { static const JsonNode defaultFS = genDefaultFS(); - if (!conf["filesystem"].isNull()) - return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), conf["filesystem"]); + if (!conf.isNull()) + return CResourceHandler::createFileSystem(getModDirectory(modName), conf); else - return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), defaultFS); -} - -static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoader * filesystem) -{ - boost::crc_32_type modChecksum; - // first - add current VCMI version into checksum to force re-validation on VCMI updates - modChecksum.process_bytes(reinterpret_cast(GameConstants::VCMI_VERSION.data()), GameConstants::VCMI_VERSION.size()); - - // second - add mod.json into checksum because filesystem does not contains this file - // FIXME: remove workaround for core mod - if (modName != ModScope::scopeBuiltin()) - { - auto modConfFile = CModInfo::getModFile(modName); - ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); - modChecksum.process_bytes(reinterpret_cast(&configChecksum), sizeof(configChecksum)); - } - // third - add all detected text files from this mod into checksum - auto files = filesystem->getFilteredFiles([](const ResourcePath & resID) - { - return (resID.getType() == EResType::TEXT || resID.getType() == EResType::JSON) && - ( boost::starts_with(resID.getName(), "DATA") || boost::starts_with(resID.getName(), "CONFIG")); - }); - - for (const ResourcePath & file : files) - { - ui32 fileChecksum = filesystem->load(file)->calculateCRC32(); - modChecksum.process_bytes(reinterpret_cast(&fileChecksum), sizeof(fileChecksum)); - } - return modChecksum.checksum(); + return CResourceHandler::createFileSystem(getModDirectory(modName), defaultFS); } void CModHandler::loadModFilesystems() { CGeneralTextHandler::detectInstallParameters(); - activeMods = validateAndSortDependencies(activeMods); + const auto & activeMods = modManager->getActiveMods(); - coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin()))); + std::map modFilesystems; - std::map modFilesystems; + for(const TModID & modName : activeMods) + modFilesystems[modName] = genModFilesystem(modName, getModInfo(modName).getFilesystemConfig()); - for(std::string & modName : activeMods) - modFilesystems[modName] = genModFilesystem(modName, allMods[modName].config); - - for(std::string & modName : activeMods) - CResourceHandler::addFilesystem("data", modName, modFilesystems[modName]); + for(const TModID & modName : activeMods) + if (modName != "core") // virtual mod 'core' has no filesystem on its own - shared with base install + CResourceHandler::addFilesystem("data", modName, modFilesystems[modName]); if (settings["mods"]["validation"].String() == "full") + checkModFilesystemsConflicts(modFilesystems); +} + +void CModHandler::checkModFilesystemsConflicts(const std::map & modFilesystems) +{ + for(const auto & [leftName, leftFilesystem] : modFilesystems) { - for(std::string & leftModName : activeMods) + for(const auto & [rightName, rightFilesystem] : modFilesystems) { - for(std::string & rightModName : activeMods) + if (leftName == rightName) + continue; + + if (getModDependencies(leftName).count(rightName) || getModDependencies(rightName).count(leftName)) + continue; + + if (getModSoftDependencies(leftName).count(rightName) || getModSoftDependencies(rightName).count(leftName)) + continue; + + const auto & filter = [](const ResourcePath &path){return path.getType() != EResType::DIRECTORY && path.getType() != EResType::JSON;}; + + std::unordered_set leftResources = leftFilesystem->getFilteredFiles(filter); + std::unordered_set rightResources = rightFilesystem->getFilteredFiles(filter); + + for (auto const & leftFile : leftResources) { - if (leftModName == rightModName) - continue; - - if (getModDependencies(leftModName).count(rightModName) || getModDependencies(rightModName).count(leftModName)) - continue; - - if (getModSoftDependencies(leftModName).count(rightModName) || getModSoftDependencies(rightModName).count(leftModName)) - continue; - - const auto & filter = [](const ResourcePath &path){return path.getType() != EResType::DIRECTORY && path.getType() != EResType::JSON;}; - - std::unordered_set leftResources = modFilesystems[leftModName]->getFilteredFiles(filter); - std::unordered_set rightResources = modFilesystems[rightModName]->getFilteredFiles(filter); - - for (auto const & leftFile : leftResources) - { - if (rightResources.count(leftFile)) - logMod->warn("Potential confict detected between '%s' and '%s': both mods add file '%s'", leftModName, rightModName, leftFile.getOriginalName()); - } + if (rightResources.count(leftFile)) + logMod->warn("Potential confict detected between '%s' and '%s': both mods add file '%s'", leftName, rightName, leftFile.getOriginalName()); } } } @@ -404,7 +133,8 @@ TModID CModHandler::findResourceOrigin(const ResourcePath & name) const { try { - for(const auto & modID : boost::adaptors::reverse(activeMods)) + auto activeMode = modManager->getActiveMods(); + for(const auto & modID : boost::adaptors::reverse(activeMode)) { if(CResourceHandler::get(modID)->existsResource(name)) return modID; @@ -459,7 +189,7 @@ std::string CModHandler::getModLanguage(const TModID& modId) const return VLC->generaltexth->getInstalledLanguage(); if(modId == "map") return VLC->generaltexth->getPreferredLanguage(); - return allMods.at(modId).baseLanguage; + return getModInfo(modId).getBaseLanguage(); } std::set CModHandler::getModDependencies(const TModID & modId) const @@ -470,11 +200,9 @@ std::set CModHandler::getModDependencies(const TModID & modId) const std::set CModHandler::getModDependencies(const TModID & modId, bool & isModFound) const { - auto it = allMods.find(modId); - isModFound = (it != allMods.end()); - - if(isModFound) - return it->second.dependencies; + isModFound = modManager->isModActive(modId); + if (isModFound) + return modManager->getModDescription(modId).getDependencies(); logMod->error("Mod not found: '%s'", modId); return {}; @@ -482,54 +210,37 @@ std::set CModHandler::getModDependencies(const TModID & modId, bool & is std::set CModHandler::getModSoftDependencies(const TModID & modId) const { - auto it = allMods.find(modId); - if(it != allMods.end()) - return it->second.softDependencies; - logMod->error("Mod not found: '%s'", modId); - return {}; + return modManager->getModDescription(modId).getSoftDependencies(); } std::set CModHandler::getModEnabledSoftDependencies(const TModID & modId) const { std::set softDependencies = getModSoftDependencies(modId); - for (auto it = softDependencies.begin(); it != softDependencies.end();) - { - if (allMods.find(*it) == allMods.end()) - it = softDependencies.erase(it); - else - it++; - } + + vstd::erase_if(softDependencies, [this](const TModID & dependency){ return !modManager->isModActive(dependency);}); + return softDependencies; } void CModHandler::initializeConfig() { - VLC->settingsHandler->loadBase(JsonUtils::assembleFromFiles(coreMod->config["settings"])); - - for(const TModID & modName : activeMods) + for(const TModID & modName : getActiveMods()) { - const auto & mod = allMods[modName]; - if (!mod.config["settings"].isNull()) - VLC->settingsHandler->loadBase(mod.config["settings"]); + const auto & mod = getModInfo(modName); + if (!mod.getLocalConfig()["settings"].isNull()) + VLC->settingsHandler->loadBase(mod.getLocalConfig()["settings"]); } } -CModVersion CModHandler::getModVersion(TModID modName) const -{ - if (allMods.count(modName)) - return allMods.at(modName).getVerificationInfo().version; - return {}; -} - void CModHandler::loadTranslation(const TModID & modName) { - const auto & mod = allMods[modName]; + const auto & mod = getModInfo(modName); std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); - std::string modBaseLanguage = allMods[modName].baseLanguage; + std::string modBaseLanguage = getModInfo(modName).getBaseLanguage(); - JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.config["translations"]); - JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.config[preferredLanguage]["translations"]); + JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.getLocalConfig()["translations"]); + JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.getLocalConfig()[preferredLanguage]["translations"]); VLC->generaltexth->loadTranslationOverrides(modName, modBaseLanguage, baseTranslation); VLC->generaltexth->loadTranslationOverrides(modName, preferredLanguage, extraTranslation); @@ -537,29 +248,45 @@ void CModHandler::loadTranslation(const TModID & modName) void CModHandler::load() { - CStopWatch totalTime; - CStopWatch timer; - - logMod->info("\tInitializing content handler: %d ms", timer.getDiff()); + logMod->info("\tInitializing content handler"); content->init(); + const auto & activeMods = getActiveMods(); + + validationPassed.insert(activeMods.begin(), activeMods.end()); + for(const TModID & modName : activeMods) { - logMod->trace("Generating checksum for %s", modName); - allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(modName))); + modChecksums[modName] = this->modManager->computeChecksum(modName); } - // first - load virtual builtin mod that contains all data - // TODO? move all data into real mods? RoE, AB, SoD, WoG - content->preloadData(*coreMod); for(const TModID & modName : activeMods) - content->preloadData(allMods[modName]); - logMod->info("\tParsing mod data: %d ms", timer.getDiff()); + { + const auto & modInfo = getModInfo(modName); + bool isValid = content->preloadData(modInfo, isModValidationNeeded(modInfo)); + if (isValid) + logGlobal->info("\t\tParsing mod: OK (%s)", modInfo.getID()); + else + logGlobal->warn("\t\tParsing mod: Issues found! (%s)", modInfo.getID()); + + if (!isValid) + validationPassed.erase(modName); + } + logMod->info("\tParsing mod data"); - content->load(*coreMod); for(const TModID & modName : activeMods) - content->load(allMods[modName]); + { + const auto & modInfo = getModInfo(modName); + bool isValid = content->load(getModInfo(modName), isModValidationNeeded(getModInfo(modName))); + if (isValid) + logGlobal->info("\t\tLoading mod: OK (%s)", modInfo.getID()); + else + logGlobal->warn("\t\tLoading mod: Issues found! (%s)", modInfo.getID()); + + if (!isValid) + validationPassed.erase(modName); + } #if SCRIPTING_ENABLED VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load @@ -570,33 +297,42 @@ void CModHandler::load() for(const TModID & modName : activeMods) loadTranslation(modName); - logMod->info("\tLoading mod data: %d ms", timer.getDiff()); + logMod->info("\tLoading mod data"); VLC->creh->loadCrExpMod(); VLC->identifiersHandler->finalize(); - logMod->info("\tResolving identifiers: %d ms", timer.getDiff()); + logMod->info("\tResolving identifiers"); content->afterLoadFinalization(); - logMod->info("\tHandlers post-load finalization: %d ms ", timer.getDiff()); - logMod->info("\tAll game content loaded in %d ms", totalTime.getDiff()); + logMod->info("\tHandlers post-load finalization"); + logMod->info("\tAll game content loaded"); } void CModHandler::afterLoad(bool onlyEssential) { JsonNode modSettings; - for (auto & modEntry : allMods) + for (const auto & modEntry : getActiveMods()) { - std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/"); - - modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); + if (validationPassed.count(modEntry)) + modManager->setValidatedChecksum(modEntry, modChecksums.at(modEntry)); + else + modManager->setValidatedChecksum(modEntry, std::nullopt); } - modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData(); - modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files"; - if(!onlyEssential) - { - std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); - file << modSettings.toString(); - } + modManager->saveConfigurationState(); +} + +bool CModHandler::isModValidationNeeded(const ModDescription & mod) const +{ + if (settings["mods"]["validation"].String() == "full") + return true; + + if (modManager->getValidatedChecksum(mod.getID()) == modChecksums.at(mod.getID())) + return false; + + if (settings["mods"]["validation"].String() == "off") + return false; + + return true; } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index e50a70f9f..e991459c8 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -12,51 +12,30 @@ VCMI_LIB_NAMESPACE_BEGIN class CModHandler; -class CModIdentifier; -class CModInfo; -struct CModVersion; -class JsonNode; -class IHandlerBase; -class CIdentifierStorage; +class ModDescription; class CContentHandler; -struct ModVerificationInfo; class ResourcePath; -class MetaString; +class ModManager; +class ISimpleResourceLoader; using TModID = std::string; class DLL_LINKAGE CModHandler final : boost::noncopyable { - std::map allMods; - std::vector activeMods;//active mods, in order in which they were loaded - std::unique_ptr coreMod; - mutable std::unique_ptr modLoadErrors; + std::unique_ptr modManager; + std::map modChecksums; + std::set validationPassed; - bool hasCircularDependency(const TModID & mod, std::set currentList = std::set()) const; - - /** - * 1. Set apart mods with resolved dependencies from mods which have unresolved dependencies - * 2. Sort resolved mods using topological algorithm - * 3. Log all problem mods and their unresolved dependencies - * - * @param modsToResolve list of valid mod IDs (checkDependencies returned true - TODO: Clarify it.) - * @return a vector of the topologically sorted resolved mods: child nodes (dependent mods) have greater index than parents - */ - std::vector validateAndSortDependencies(std::vector modsToResolve) const; - - std::vector getModList(const std::string & path) const; - void loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods); - void loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods); void loadTranslation(const TModID & modName); + void checkModFilesystemsConflicts(const std::map & modFilesystems); - CModVersion getModVersion(TModID modName) const; + bool isModValidationNeeded(const ModDescription & mod) const; public: - std::shared_ptr content; //(!)Do not serialize FIXME: make private + std::shared_ptr content; /// receives list of available mods and trying to load mod.json from all of them void initializeConfig(); - void loadMods(); void loadModFilesystems(); /// returns ID of mod that provides selected file resource @@ -77,12 +56,9 @@ public: /// returns list of all (active) mods std::vector getAllMods() const; - std::vector getActiveMods() const; + const std::vector & getActiveMods() const; - /// Returns human-readable string that describes errors encounter during mod loading, such as missing dependencies - std::string getModLoadErrors() const; - - const CModInfo & getModInfo(const TModID & modId) const; + const ModDescription & getModInfo(const TModID & modId) const; /// load content from all available mods void load(); diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp deleted file mode 100644 index fb0778f6f..000000000 --- a/lib/modding/CModInfo.cpp +++ /dev/null @@ -1,206 +0,0 @@ -/* - * CModInfo.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "CModInfo.h" - -#include "../texts/CGeneralTextHandler.h" -#include "../VCMI_Lib.h" -#include "../filesystem/Filesystem.h" - -VCMI_LIB_NAMESPACE_BEGIN - -static JsonNode addMeta(JsonNode config, const std::string & meta) -{ - config.setModScope(meta); - return config; -} - -std::set CModInfo::readModList(const JsonNode & input) -{ - std::set result; - - for (auto const & string : input.convertTo>()) - result.insert(boost::to_lower_copy(string)); - - return result; -} - -CModInfo::CModInfo(): - explicitlyEnabled(false), - implicitlyEnabled(true), - validation(PENDING) -{ - -} - -CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): - identifier(identifier), - dependencies(readModList(config["depends"])), - softDependencies(readModList(config["softDepends"])), - conflicts(readModList(config["conflicts"])), - explicitlyEnabled(false), - implicitlyEnabled(true), - validation(PENDING), - config(addMeta(config, identifier)) -{ - if (!config["name"].String().empty()) - verificationInfo.name = config["name"].String(); - else - verificationInfo.name = identifier; - - verificationInfo.version = CModVersion::fromString(config["version"].String()); - verificationInfo.parent = identifier.substr(0, identifier.find_last_of('.')); - if(verificationInfo.parent == identifier) - verificationInfo.parent.clear(); - - if(!config["compatibility"].isNull()) - { - vcmiCompatibleMin = CModVersion::fromString(config["compatibility"]["min"].String()); - vcmiCompatibleMax = CModVersion::fromString(config["compatibility"]["max"].String()); - } - - if (!config["language"].isNull()) - baseLanguage = config["language"].String(); - else - baseLanguage = "english"; - - loadLocalData(local); -} - -JsonNode CModInfo::saveLocalData() const -{ - std::ostringstream stream; - stream << std::noshowbase << std::hex << std::setw(8) << std::setfill('0') << verificationInfo.checksum; - - JsonNode conf; - conf["active"].Bool() = explicitlyEnabled; - conf["validated"].Bool() = validation != FAILED; - conf["checksum"].String() = stream.str(); - return conf; -} - -std::string CModInfo::getModDir(const std::string & name) -{ - return "MODS/" + boost::algorithm::replace_all_copy(name, ".", "/MODS/"); -} - -JsonPath CModInfo::getModFile(const std::string & name) -{ - return JsonPath::builtinTODO(getModDir(name) + "/mod.json"); -} - -void CModInfo::updateChecksum(ui32 newChecksum) -{ - // comment-out next line to force validation of all mods ignoring checksum - if (newChecksum != verificationInfo.checksum) - { - verificationInfo.checksum = newChecksum; - validation = PENDING; - } -} - -void CModInfo::loadLocalData(const JsonNode & data) -{ - bool validated = false; - implicitlyEnabled = true; - explicitlyEnabled = !config["keepDisabled"].Bool(); - verificationInfo.checksum = 0; - if (data.isStruct()) - { - explicitlyEnabled = data["active"].Bool(); - validated = data["validated"].Bool(); - updateChecksum(strtol(data["checksum"].String().c_str(), nullptr, 16)); - } - - //check compatibility - implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin, true, true)); - implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true, true)); - - if(!implicitlyEnabled) - logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", verificationInfo.name); - - if (config["modType"].String() == "Translation") - { - if (baseLanguage != CGeneralTextHandler::getPreferredLanguage()) - { - if (identifier.find_last_of('.') == std::string::npos) - logGlobal->warn("Translation mod %s was not loaded: language mismatch!", verificationInfo.name); - implicitlyEnabled = false; - } - } - if (config["modType"].String() == "Compatibility") - { - // compatibility mods are always explicitly enabled - // however they may be implicitly disabled - if one of their dependencies is missing - explicitlyEnabled = true; - } - - if (isEnabled()) - validation = validated ? PASSED : PENDING; - else - validation = validated ? PASSED : FAILED; - - verificationInfo.impactsGameplay = checkModGameplayAffecting(); -} - -bool CModInfo::checkModGameplayAffecting() const -{ - if (modGameplayAffecting.has_value()) - return *modGameplayAffecting; - - static const std::vector keysToTest = { - "heroClasses", - "artifacts", - "creatures", - "factions", - "objects", - "heroes", - "spells", - "skills", - "templates", - "scripts", - "battlefields", - "terrains", - "rivers", - "roads", - "obstacles" - }; - - JsonPath modFileResource(CModInfo::getModFile(identifier)); - - if(CResourceHandler::get("initial")->existsResource(modFileResource)) - { - const JsonNode modConfig(modFileResource); - - for(const auto & key : keysToTest) - { - if (!modConfig[key].isNull()) - { - modGameplayAffecting = true; - return *modGameplayAffecting; - } - } - } - modGameplayAffecting = false; - return *modGameplayAffecting; -} - -const ModVerificationInfo & CModInfo::getVerificationInfo() const -{ - assert(!verificationInfo.name.empty()); - return verificationInfo; -} - -bool CModInfo::isEnabled() const -{ - return implicitlyEnabled && explicitlyEnabled; -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h deleted file mode 100644 index d5c077167..000000000 --- a/lib/modding/CModInfo.h +++ /dev/null @@ -1,85 +0,0 @@ -/* - * CModInfo.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include "../json/JsonNode.h" -#include "ModVerificationInfo.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE CModInfo -{ - /// cached result of checkModGameplayAffecting() call - /// Do not serialize - depends on local mod version, not server/save mod version - mutable std::optional modGameplayAffecting; - - static std::set readModList(const JsonNode & input); -public: - enum EValidationStatus - { - PENDING, - FAILED, - PASSED - }; - - /// identifier, identical to name of folder with mod - std::string identifier; - - /// detailed mod description - std::string description; - - /// Base language of mod, all mod strings are assumed to be in this language - std::string baseLanguage; - - /// vcmi versions compatible with the mod - CModVersion vcmiCompatibleMin, vcmiCompatibleMax; - - /// list of mods that should be loaded before this one - std::set dependencies; - - /// list of mods if they are enabled, should be loaded before this one. this mod will overwrite any conflicting items from its soft dependency mods - std::set softDependencies; - - /// list of mods that can't be used in the same time as this one - std::set conflicts; - - EValidationStatus validation; - - JsonNode config; - - CModInfo(); - CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config); - - JsonNode saveLocalData() const; - void updateChecksum(ui32 newChecksum); - - bool isEnabled() const; - - static std::string getModDir(const std::string & name); - static JsonPath getModFile(const std::string & name); - - /// return true if this mod can affect gameplay, e.g. adds or modifies any game objects - bool checkModGameplayAffecting() const; - - const ModVerificationInfo & getVerificationInfo() const; - -private: - /// true if mod is enabled by user, e.g. in Launcher UI - bool explicitlyEnabled; - - /// true if mod can be loaded - compatible and has no missing deps - bool implicitlyEnabled; - - ModVerificationInfo verificationInfo; - - void loadLocalData(const JsonNode & data); -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index 2696080fe..380e98d18 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -11,7 +11,8 @@ #include "ContentTypeHandler.h" #include "CModHandler.h" -#include "CModInfo.h" +#include "ModDescription.h" +#include "ModManager.h" #include "ModScope.h" #include "../BattleFieldHandler.h" @@ -54,7 +55,7 @@ ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string bool ContentTypeHandler::preloadModData(const std::string & modName, const JsonNode & fileList, bool validate) { - bool result = false; + bool result = true; JsonNode data = JsonUtils::assembleFromFiles(fileList, result); data.setModScope(modName); @@ -170,7 +171,7 @@ void ContentTypeHandler::afterLoadFinalization() { if (data.second.modData.isNull()) { - for (auto node : data.second.patches.Struct()) + for (const auto & node : data.second.patches.Struct()) logMod->warn("Mod '%s' have added patch for object '%s' from mod '%s', but this mod was not loaded or has no new objects.", node.second.getModScope(), node.first, data.first); } @@ -258,22 +259,26 @@ void CContentHandler::init() handlers.insert(std::make_pair("biomes", ContentTypeHandler(VLC->biomeHandler.get(), "biome"))); } -bool CContentHandler::preloadModData(const std::string & modName, JsonNode modConfig, bool validate) +bool CContentHandler::preloadData(const ModDescription & mod, bool validate) { bool result = true; + + if (!JsonUtils::validate(mod.getLocalConfig(), "vcmi:mod", mod.getID())) + result = false; + for(auto & handler : handlers) { - result &= handler.second.preloadModData(modName, modConfig[handler.first], validate); + result &= handler.second.preloadModData(mod.getID(), mod.getLocalValue(handler.first), validate); } return result; } -bool CContentHandler::loadMod(const std::string & modName, bool validate) +bool CContentHandler::load(const ModDescription & mod, bool validate) { bool result = true; for(auto & handler : handlers) { - result &= handler.second.loadMod(modName, validate); + result &= handler.second.loadMod(mod.getID(), validate); } return result; } @@ -294,58 +299,9 @@ void CContentHandler::afterLoadFinalization() } } -void CContentHandler::preloadData(CModInfo & mod) -{ - bool validate = validateMod(mod); - - // print message in format [<8-symbols checksum>] - auto & info = mod.getVerificationInfo(); - logMod->info("\t\t[%08x]%s", info.checksum, info.name); - - if (validate && mod.identifier != ModScope::scopeBuiltin()) - { - if (!JsonUtils::validate(mod.config, "vcmi:mod", mod.identifier)) - mod.validation = CModInfo::FAILED; - } - if (!preloadModData(mod.identifier, mod.config, validate)) - mod.validation = CModInfo::FAILED; -} - -void CContentHandler::load(CModInfo & mod) -{ - bool validate = validateMod(mod); - - if (!loadMod(mod.identifier, validate)) - mod.validation = CModInfo::FAILED; - - if (validate) - { - if (mod.validation != CModInfo::FAILED) - logMod->info("\t\t[DONE] %s", mod.getVerificationInfo().name); - else - logMod->error("\t\t[FAIL] %s", mod.getVerificationInfo().name); - } - else - logMod->info("\t\t[SKIP] %s", mod.getVerificationInfo().name); -} - const ContentTypeHandler & CContentHandler::operator[](const std::string & name) const { return handlers.at(name); } -bool CContentHandler::validateMod(const CModInfo & mod) const -{ - if (settings["mods"]["validation"].String() == "full") - return true; - - if (mod.validation == CModInfo::PASSED) - return false; - - if (settings["mods"]["validation"].String() == "off") - return false; - - return true; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ContentTypeHandler.h b/lib/modding/ContentTypeHandler.h index a623cb226..abcf7321e 100644 --- a/lib/modding/ContentTypeHandler.h +++ b/lib/modding/ContentTypeHandler.h @@ -14,7 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN class IHandlerBase; -class CModInfo; +class ModDescription; /// internal type to handle loading of one data type (e.g. artifacts, creatures) class DLL_LINKAGE ContentTypeHandler @@ -50,23 +50,16 @@ public: /// class used to load all game data into handlers. Used only during loading class DLL_LINKAGE CContentHandler { - /// preloads all data from fileList as data from modName. - bool preloadModData(const std::string & modName, JsonNode modConfig, bool validate); - - /// actually loads data in mod - bool loadMod(const std::string & modName, bool validate); - std::map handlers; - bool validateMod(const CModInfo & mod) const; public: void init(); /// preloads all data from fileList as data from modName. - void preloadData(CModInfo & mod); + bool preloadData(const ModDescription & mod, bool validateMod); /// actually loads data in mod - void load(CModInfo & mod); + bool load(const ModDescription & mod, bool validateMod); void loadCustom(); diff --git a/lib/modding/ModDescription.cpp b/lib/modding/ModDescription.cpp new file mode 100644 index 000000000..d27253ea3 --- /dev/null +++ b/lib/modding/ModDescription.cpp @@ -0,0 +1,233 @@ +/* + * ModDescription.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "ModDescription.h" + +#include "CModVersion.h" +#include "ModVerificationInfo.h" + +#include "../json/JsonNode.h" +#include "../texts/CGeneralTextHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ModDescription::ModDescription(const TModID & fullID, const JsonNode & localConfig, const JsonNode & repositoryConfig) + : identifier(fullID) + , localConfig(std::make_unique(localConfig)) + , repositoryConfig(std::make_unique(repositoryConfig)) + , dependencies(loadModList(getValue("depends"))) + , softDependencies(loadModList(getValue("softDepends"))) + , conflicts(loadModList(getValue("conflicts"))) +{ + if(getID() != "core") + dependencies.emplace("core"); + + if (!getParentID().empty()) + dependencies.emplace(getParentID()); +} + +ModDescription::~ModDescription() = default; + +TModSet ModDescription::loadModList(const JsonNode & configNode) const +{ + TModSet result; + for(const auto & entry : configNode.Vector()) + result.insert(boost::algorithm::to_lower_copy(entry.String())); + return result; +} + +const TModID & ModDescription::getID() const +{ + return identifier; +} + +TModID ModDescription::getParentID() const +{ + size_t dotPos = identifier.find_last_of('.'); + + if(dotPos == std::string::npos) + return {}; + + return identifier.substr(0, dotPos); +} + +TModID ModDescription::getTopParentID() const +{ + size_t dotPos = identifier.find('.'); + + if(dotPos == std::string::npos) + return {}; + + return identifier.substr(0, dotPos); +} + +const TModSet & ModDescription::getDependencies() const +{ + return dependencies; +} + +const TModSet & ModDescription::getSoftDependencies() const +{ + return softDependencies; +} + +const TModSet & ModDescription::getConflicts() const +{ + return conflicts; +} + +const std::string & ModDescription::getBaseLanguage() const +{ + static const std::string defaultLanguage = "english"; + + return getValue("language").isString() ? getValue("language").String() : defaultLanguage; +} + +const std::string & ModDescription::getName() const +{ + return getLocalizedValue("name").String(); +} + +const JsonNode & ModDescription::getFilesystemConfig() const +{ + return getLocalValue("filesystem"); +} + +const JsonNode & ModDescription::getLocalConfig() const +{ + return *localConfig; +} + +const JsonNode & ModDescription::getLocalizedValue(const std::string & keyName) const +{ + const std::string language = CGeneralTextHandler::getPreferredLanguage(); + const JsonNode & languageNode = getValue(language); + const JsonNode & baseValue = getValue(keyName); + const JsonNode & localizedValue = languageNode[keyName]; + + if (localizedValue.isNull()) + return baseValue; + else + return localizedValue; +} + +const JsonNode & ModDescription::getValue(const std::string & keyName) const +{ + const JsonNode & localValue = getLocalValue(keyName); + if (localValue.isNull()) + return getRepositoryValue(keyName); + else + return getLocalValue(keyName); +} + +const JsonNode & ModDescription::getLocalValue(const std::string & keyName) const +{ + return getLocalConfig()[keyName]; +} + +const JsonNode & ModDescription::getRepositoryValue(const std::string & keyName) const +{ + return (*repositoryConfig)[keyName]; +} + +CModVersion ModDescription::getVersion() const +{ + return CModVersion::fromString(getValue("version").String()); +} + +ModVerificationInfo ModDescription::getVerificationInfo() const +{ + ModVerificationInfo result; + result.name = getName(); + result.version = getVersion(); + result.impactsGameplay = affectsGameplay(); + result.parent = getParentID(); + + return result; +} + +bool ModDescription::isCompatible() const +{ + const JsonNode & compatibility = getValue("compatibility"); + + if (compatibility.isNull()) + return true; + + auto vcmiCompatibleMin = CModVersion::fromString(compatibility["min"].String()); + auto vcmiCompatibleMax = CModVersion::fromString(compatibility["max"].String()); + + bool compatible = true; + compatible &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin, true, true)); + compatible &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true, true)); + + return compatible; +} + +bool ModDescription::isCompatibility() const +{ + return getValue("modType").String() == "Compatibility"; +} + +bool ModDescription::isTranslation() const +{ + return getValue("modType").String() == "Translation"; +} + +bool ModDescription::keepDisabled() const +{ + return getValue("keepDisabled").Bool(); +} + +bool ModDescription::isInstalled() const +{ + return !localConfig->isNull(); +} + +bool ModDescription::affectsGameplay() const +{ + static const std::array keysToTest = { + "artifacts", + "battlefields", + "creatures", + "factions", + "heroClasses", + "heroes", + "objects", + "obstacles", + "rivers", + "roads", + "settings", + "skills", + "spells", + "terrains", + }; + + for(const auto & key : keysToTest) + if (!getLocalValue(key).isNull()) + return true; + + return false; +} + +bool ModDescription::isUpdateAvailable() const +{ + if (getRepositoryValue("version").isNull()) + return false; + + if (getLocalValue("version").isNull()) + return false; + + auto localVersion = CModVersion::fromString(getLocalValue("version").String()); + auto repositoryVersion = CModVersion::fromString(getRepositoryValue("version").String()); + + return localVersion < repositoryVersion; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModDescription.h b/lib/modding/ModDescription.h new file mode 100644 index 000000000..225cf953e --- /dev/null +++ b/lib/modding/ModDescription.h @@ -0,0 +1,70 @@ +/* + * ModDescription.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN + +struct CModVersion; +struct ModVerificationInfo; +class JsonNode; + +using TModID = std::string; +using TModList = std::vector; +using TModSet = std::set; + +class DLL_LINKAGE ModDescription : boost::noncopyable +{ + TModID identifier; + + std::unique_ptr localConfig; + std::unique_ptr repositoryConfig; + + TModSet dependencies; + TModSet softDependencies; + TModSet conflicts; + + TModSet loadModList(const JsonNode & configNode) const; + +public: + ModDescription(const TModID & fullID, const JsonNode & localConfig, const JsonNode & repositoryConfig); + ~ModDescription(); + + const TModID & getID() const; + TModID getParentID() const; + TModID getTopParentID() const; + + const TModSet & getDependencies() const; + const TModSet & getSoftDependencies() const; + const TModSet & getConflicts() const; + + const std::string & getBaseLanguage() const; + const std::string & getName() const; + + const JsonNode & getFilesystemConfig() const; + const JsonNode & getLocalConfig() const; + const JsonNode & getValue(const std::string & keyName) const; + const JsonNode & getLocalizedValue(const std::string & keyName) const; + const JsonNode & getLocalValue(const std::string & keyName) const; + const JsonNode & getRepositoryValue(const std::string & keyName) const; + + CModVersion getVersion() const; + ModVerificationInfo getVerificationInfo() const; + + bool isCompatible() const; + bool isUpdateAvailable() const; + + bool affectsGameplay() const; + bool isCompatibility() const; + bool isTranslation() const; + bool keepDisabled() const; + bool isInstalled() const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModManager.cpp b/lib/modding/ModManager.cpp new file mode 100644 index 000000000..f18452def --- /dev/null +++ b/lib/modding/ModManager.cpp @@ -0,0 +1,706 @@ +/* + * ModManager.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "ModManager.h" + +#include "ModDescription.h" +#include "ModScope.h" + +#include "../constants/StringConstants.h" +#include "../filesystem/Filesystem.h" +#include "../json/JsonNode.h" +#include "../texts/CGeneralTextHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static std::string getModDirectory(const TModID & modName) +{ + std::string result = modName; + boost::to_upper(result); + boost::algorithm::replace_all(result, ".", "/MODS/"); + return "MODS/" + result; +} + +static std::string getModSettingsDirectory(const TModID & modName) +{ + return getModDirectory(modName) + "/MODS/"; +} + +static JsonPath getModDescriptionFile(const TModID & modName) +{ + return JsonPath::builtin(getModDirectory(modName) + "/mod"); +} + +ModsState::ModsState() +{ + modList.push_back(ModScope::scopeBuiltin()); + + std::vector testLocations = scanModsDirectory("MODS/"); + + while(!testLocations.empty()) + { + std::string target = testLocations.back(); + testLocations.pop_back(); + modList.push_back(boost::algorithm::to_lower_copy(target)); + + for(const auto & submod : scanModsDirectory(getModSettingsDirectory(target))) + testLocations.push_back(target + '.' + submod); + } +} + +TModList ModsState::getInstalledMods() const +{ + return modList; +} + +uint32_t ModsState::computeChecksum(const TModID & modName) const +{ + boost::crc_32_type modChecksum; + // first - add current VCMI version into checksum to force re-validation on VCMI updates + modChecksum.process_bytes(static_cast(GameConstants::VCMI_VERSION.data()), GameConstants::VCMI_VERSION.size()); + + // second - add mod.json into checksum because filesystem does not contains this file + if (modName != ModScope::scopeBuiltin()) + { + auto modConfFile = getModDescriptionFile(modName); + ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); + modChecksum.process_bytes(static_cast(&configChecksum), sizeof(configChecksum)); + } + + // third - add all detected text files from this mod into checksum + const auto & filesystem = CResourceHandler::get(modName); + + auto files = filesystem->getFilteredFiles([](const ResourcePath & resID) + { + return resID.getType() == EResType::JSON && boost::starts_with(resID.getName(), "CONFIG"); + }); + + for (const ResourcePath & file : files) + { + ui32 fileChecksum = filesystem->load(file)->calculateCRC32(); + modChecksum.process_bytes(static_cast(&fileChecksum), sizeof(fileChecksum)); + } + return modChecksum.checksum(); +} + +double ModsState::getInstalledModSizeMegabytes(const TModID & modName) const +{ + ResourcePath resDir(getModDirectory(modName), EResType::DIRECTORY); + std::string path = CResourceHandler::get()->getResourceName(resDir)->string(); + + size_t sizeBytes = 0; + for(boost::filesystem::recursive_directory_iterator it(path); it != boost::filesystem::recursive_directory_iterator(); ++it) + { + if(!boost::filesystem::is_directory(*it)) + sizeBytes += boost::filesystem::file_size(*it); + } + + double sizeMegabytes = sizeBytes / static_cast(1024*1024); + return sizeMegabytes; +} + +std::vector ModsState::scanModsDirectory(const std::string & modDir) const +{ + size_t depth = boost::range::count(modDir, '/'); + + const auto & modScanFilter = [&](const ResourcePath & id) -> bool + { + if(id.getType() != EResType::DIRECTORY) + return false; + if(!boost::algorithm::starts_with(id.getName(), modDir)) + return false; + if(boost::range::count(id.getName(), '/') != depth) + return false; + return true; + }; + + auto list = CResourceHandler::get("initial")->getFilteredFiles(modScanFilter); + + //storage for found mods + std::vector foundMods; + for(const auto & entry : list) + { + std::string name = entry.getName(); + name.erase(0, modDir.size()); //Remove path prefix + + if(name.empty()) + continue; + + if(name.find('.') != std::string::npos) + continue; + + if (ModScope::isScopeReserved(boost::to_lower_copy(name))) + continue; + + if(!CResourceHandler::get("initial")->existsResource(JsonPath::builtin(entry.getName() + "/MOD"))) + continue; + + foundMods.push_back(name); + } + return foundMods; +} + +/////////////////////////////////////////////////////////////////////////////// + +ModsPresetState::ModsPresetState() +{ + static const JsonPath settingsPath = JsonPath::builtin("config/modSettings.json"); + + if(CResourceHandler::get("local")->existsResource(ResourcePath(settingsPath))) + { + modConfig = JsonNode(settingsPath); + } + else + { + // Probably new install. Create initial configuration + CResourceHandler::get("local")->createResource(settingsPath.getOriginalName() + ".json"); + } + + if(modConfig["presets"].isNull()) + { + modConfig["activePreset"] = JsonNode("default"); + if(modConfig["activeMods"].isNull()) + createInitialPreset(); // new install + else + importInitialPreset(); // 1.5 format import + } +} + +void ModsPresetState::createInitialPreset() +{ + // TODO: scan mods directory for all its content? Probably unnecessary since this looks like new install, but who knows? + modConfig["presets"]["default"]["mods"].Vector().emplace_back("vcmi"); +} + +void ModsPresetState::importInitialPreset() +{ + JsonNode preset; + + for(const auto & mod : modConfig["activeMods"].Struct()) + { + if(mod.second["active"].Bool()) + preset["mods"].Vector().emplace_back(mod.first); + + for(const auto & submod : mod.second["mods"].Struct()) + preset["settings"][mod.first][submod.first] = submod.second["active"]; + } + modConfig["presets"]["default"] = preset; +} + +const JsonNode & ModsPresetState::getActivePresetConfig() const +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + const JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + return currentPreset; +} + +TModList ModsPresetState::getActiveRootMods() const +{ + const JsonNode & modsToActivateJson = getActivePresetConfig()["mods"]; + auto modsToActivate = modsToActivateJson.convertTo>(); + if (!vstd::contains(modsToActivate, ModScope::scopeBuiltin())) + modsToActivate.push_back(ModScope::scopeBuiltin()); + return modsToActivate; +} + +std::map ModsPresetState::getModSettings(const TModID & modID) const +{ + const JsonNode & modSettingsJson = getActivePresetConfig()["settings"][modID]; + auto modSettings = modSettingsJson.convertTo>(); + return modSettings; +} + +std::optional ModsPresetState::getValidatedChecksum(const TModID & modName) const +{ + const JsonNode & node = modConfig["validatedMods"][modName]; + if (node.isNull()) + return std::nullopt; + else + return node.Integer(); +} + +void ModsPresetState::setModActive(const TModID & modID, bool isActive) +{ + size_t dotPos = modID.find('.'); + + if(dotPos != std::string::npos) + { + std::string rootMod = modID.substr(0, dotPos); + std::string settingID = modID.substr(dotPos + 1); + setSettingActive(rootMod, settingID, isActive); + } + else + { + if (isActive) + addRootMod(modID); + else + eraseRootMod(modID); + } +} + +void ModsPresetState::addRootMod(const TModID & modName) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + + if (!vstd::contains(currentPreset["mods"].Vector(), JsonNode(modName))) + currentPreset["mods"].Vector().emplace_back(modName); +} + +void ModsPresetState::setSettingActive(const TModID & modName, const TModID & settingName, bool isActive) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + + currentPreset["settings"][modName][settingName].Bool() = isActive; +} + +void ModsPresetState::removeOldMods(const TModList & modsToKeep) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + + vstd::erase_if(currentPreset["mods"].Vector(), [&](const JsonNode & entry){ + return !vstd::contains(modsToKeep, entry.String()); + }); + + vstd::erase_if(currentPreset["settings"].Struct(), [&](const auto & entry){ + return !vstd::contains(modsToKeep, entry.first); + }); +} + +void ModsPresetState::eraseRootMod(const TModID & modName) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + vstd::erase(currentPreset["mods"].Vector(), JsonNode(modName)); +} + +void ModsPresetState::eraseModSetting(const TModID & modName, const TModID & settingName) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + currentPreset["settings"][modName].Struct().erase(settingName); +} + +std::vector ModsPresetState::getActiveMods() const +{ + TModList activeRootMods = getActiveRootMods(); + TModList allActiveMods; + + for(const auto & activeMod : activeRootMods) + { + assert(!vstd::contains(allActiveMods, activeMod)); + allActiveMods.push_back(activeMod); + + for(const auto & submod : getModSettings(activeMod)) + { + if(submod.second) + { + assert(!vstd::contains(allActiveMods, activeMod + '.' + submod.first)); + allActiveMods.push_back(activeMod + '.' + submod.first); + } + } + } + return allActiveMods; +} + +void ModsPresetState::setValidatedChecksum(const TModID & modName, std::optional value) +{ + if (value.has_value()) + modConfig["validatedMods"][modName].Integer() = *value; + else + modConfig["validatedMods"].Struct().erase(modName); +} + +void ModsPresetState::saveConfigurationState() const +{ + std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); + file << modConfig.toCompactString(); +} + +ModsStorage::ModsStorage(const std::vector & modsToLoad, const JsonNode & repositoryList) +{ + JsonNode coreModConfig(JsonPath::builtin("config/gameConfig.json")); + coreModConfig.setModScope(ModScope::scopeBuiltin()); + mods.try_emplace(ModScope::scopeBuiltin(), ModScope::scopeBuiltin(), coreModConfig, JsonNode()); + + for(auto modID : modsToLoad) + { + if(ModScope::isScopeReserved(modID)) + continue; + + JsonNode modConfig(getModDescriptionFile(modID)); + modConfig.setModScope(modID); + + if(modConfig["modType"].isNull()) + { + logMod->error("Can not load mod %s - invalid mod config file!", modID); + continue; + } + + mods.try_emplace(modID, modID, modConfig, repositoryList[modID]); + } + + for(const auto & mod : repositoryList.Struct()) + { + if (vstd::contains(modsToLoad, mod.first)) + continue; + + if (mod.second["modType"].isNull() || mod.second["name"].isNull()) + continue; + + mods.try_emplace(mod.first, mod.first, JsonNode(), mod.second); + } +} + +const ModDescription & ModsStorage::getMod(const TModID & fullID) const +{ + return mods.at(fullID); +} + +TModList ModsStorage::getAllMods() const +{ + TModList result; + for (const auto & mod : mods) + result.push_back(mod.first); + + return result; +} + +ModManager::ModManager() + :ModManager(JsonNode()) +{ +} + +ModManager::ModManager(const JsonNode & repositoryList) + : modsState(std::make_unique()) + , modsPreset(std::make_unique()) +{ + modsStorage = std::make_unique(modsState->getInstalledMods(), repositoryList); + + eraseMissingModsFromPreset(); + addNewModsToPreset(); + + std::vector desiredModList = modsPreset->getActiveMods(); + ModDependenciesResolver newResolver(desiredModList, *modsStorage); + updatePreset(newResolver); +} + +ModManager::~ModManager() = default; + +const ModDescription & ModManager::getModDescription(const TModID & modID) const +{ + assert(boost::to_lower_copy(modID) == modID); + return modsStorage->getMod(modID); +} + +bool ModManager::isModSettingActive(const TModID & rootModID, const TModID & modSettingID) const +{ + return modsPreset->getModSettings(rootModID).at(modSettingID); +} + +bool ModManager::isModActive(const TModID & modID) const +{ + return vstd::contains(getActiveMods(), modID); +} + +const TModList & ModManager::getActiveMods() const +{ + return depedencyResolver->getActiveMods(); +} + +uint32_t ModManager::computeChecksum(const TModID & modName) const +{ + return modsState->computeChecksum(modName); +} + +std::optional ModManager::getValidatedChecksum(const TModID & modName) const +{ + return modsPreset->getValidatedChecksum(modName); +} + +void ModManager::setValidatedChecksum(const TModID & modName, std::optional value) +{ + modsPreset->setValidatedChecksum(modName, value); +} + +void ModManager::saveConfigurationState() const +{ + modsPreset->saveConfigurationState(); +} + +TModList ModManager::getAllMods() const +{ + return modsStorage->getAllMods(); +} + +double ModManager::getInstalledModSizeMegabytes(const TModID & modName) const +{ + return modsState->getInstalledModSizeMegabytes(modName); +} + +void ModManager::eraseMissingModsFromPreset() +{ + const TModList & installedMods = modsState->getInstalledMods(); + const TModList & rootMods = modsPreset->getActiveRootMods(); + + modsPreset->removeOldMods(installedMods); + + for(const auto & rootMod : rootMods) + { + const auto & modSettings = modsPreset->getModSettings(rootMod); + + for(const auto & modSetting : modSettings) + { + TModID fullModID = rootMod + '.' + modSetting.first; + if(!vstd::contains(installedMods, fullModID)) + { + modsPreset->eraseModSetting(rootMod, modSetting.first); + continue; + } + } + } +} + +void ModManager::addNewModsToPreset() +{ + const TModList & installedMods = modsState->getInstalledMods(); + + for(const auto & modID : installedMods) + { + size_t dotPos = modID.find('.'); + + if(dotPos == std::string::npos) + continue; // only look up submods aka mod settings + + std::string rootMod = modID.substr(0, dotPos); + std::string settingID = modID.substr(dotPos + 1); + + const auto & modSettings = modsPreset->getModSettings(rootMod); + + if (!modSettings.count(settingID)) + modsPreset->setSettingActive(rootMod, settingID, !modsStorage->getMod(modID).keepDisabled()); + } +} + +TModList ModManager::collectDependenciesRecursive(const TModID & modID) const +{ + TModList result; + TModList toTest; + + toTest.push_back(modID); + while (!toTest.empty()) + { + TModID currentModID = toTest.back(); + const auto & currentMod = getModDescription(currentModID); + toTest.pop_back(); + result.push_back(currentModID); + + if (!currentMod.isInstalled()) + throw std::runtime_error("Unable to enable mod " + modID + "! Dependency " + currentModID + " is not installed!"); + + for (const auto & dependency : currentMod.getDependencies()) + { + if (!vstd::contains(result, dependency)) + toTest.push_back(dependency); + } + } + + return result; +} + +void ModManager::tryEnableMods(const TModList & modList) +{ + TModList requiredActiveMods; + TModList additionalActiveMods = getActiveMods(); + + for (const auto & modName : modList) + { + for (const auto & dependency : collectDependenciesRecursive(modName)) + { + if (!vstd::contains(requiredActiveMods, dependency)) + { + requiredActiveMods.push_back(dependency); + vstd::erase(additionalActiveMods, dependency); + } + } + + assert(!vstd::contains(additionalActiveMods, modName)); + assert(vstd::contains(requiredActiveMods, modName));// FIXME: fails on attempt to enable broken mod / translation to other language + } + + ModDependenciesResolver testResolver(requiredActiveMods, *modsStorage); + + testResolver.tryAddMods(additionalActiveMods, *modsStorage); + + TModList additionalActiveSubmods; + for (const auto & modName : modList) + { + if (modName.find('.') != std::string::npos) + continue; + + auto modSettings = modsPreset->getModSettings(modName); + for (const auto & entry : modSettings) + { + TModID fullModID = modName + '.' + entry.first; + if (entry.second && !vstd::contains(requiredActiveMods, fullModID)) + additionalActiveSubmods.push_back(fullModID); + } + } + + testResolver.tryAddMods(additionalActiveSubmods, *modsStorage); + + for (const auto & modName : modList) + if (!vstd::contains(testResolver.getActiveMods(), modName)) + throw std::runtime_error("Failed to enable mod! Mod " + modName + " remains disabled!"); + + updatePreset(testResolver); +} + +void ModManager::tryDisableMod(const TModID & modName) +{ + auto desiredActiveMods = getActiveMods(); + assert(vstd::contains(desiredActiveMods, modName)); + + vstd::erase(desiredActiveMods, modName); + + ModDependenciesResolver testResolver(desiredActiveMods, *modsStorage); + + if (vstd::contains(testResolver.getActiveMods(), modName)) + throw std::runtime_error("Failed to disable mod! Mod " + modName + " remains enabled!"); + + modsPreset->setModActive(modName, false); + updatePreset(testResolver); +} + +void ModManager::updatePreset(const ModDependenciesResolver & testResolver) +{ + const auto & newActiveMods = testResolver.getActiveMods(); + const auto & newBrokenMods = testResolver.getBrokenMods(); + + for (const auto & modID : newActiveMods) + { + assert(vstd::contains(modsState->getInstalledMods(), modID)); + modsPreset->setModActive(modID, true); + } + + for (const auto & modID : newBrokenMods) + { + const auto & mod = getModDescription(modID); + if (vstd::contains(newActiveMods, mod.getTopParentID())) + modsPreset->setModActive(modID, false); + } + + std::vector desiredModList = modsPreset->getActiveMods(); + + // Try to enable all existing compatibility patches. Ignore on failure + for (const auto & rootMod : modsPreset->getActiveRootMods()) + { + for (const auto & modSetting : modsPreset->getModSettings(rootMod)) + { + if (modSetting.second) + continue; + + TModID fullModID = rootMod + '.' + modSetting.first; + const auto & modDescription = modsStorage->getMod(fullModID); + + if (modDescription.isCompatibility()) + desiredModList.push_back(fullModID); + } + } + + depedencyResolver = std::make_unique(desiredModList, *modsStorage); + modsPreset->saveConfigurationState(); +} + +ModDependenciesResolver::ModDependenciesResolver(const TModList & modsToResolve, const ModsStorage & storage) +{ + tryAddMods(modsToResolve, storage); +} + +const TModList & ModDependenciesResolver::getActiveMods() const +{ + return activeMods; +} + +const TModList & ModDependenciesResolver::getBrokenMods() const +{ + return brokenMods; +} + +void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStorage & storage) +{ + // Topological sort algorithm. + boost::range::sort(modsToResolve); // Sort mods per name + std::vector sortedValidMods(activeMods.begin(), activeMods.end()); // Vector keeps order of elements (LIFO) + std::set resolvedModIDs(activeMods.begin(), activeMods.end()); // Use a set for validation for performance reason, but set does not keep order of elements + std::set notResolvedModIDs(modsToResolve.begin(), modsToResolve.end()); // Use a set for validation for performance reason + + // Mod is resolved if it has no dependencies or all its dependencies are already resolved + auto isResolved = [&](const ModDescription & mod) -> bool + { + if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage()) + return false; + + if(mod.getDependencies().size() > resolvedModIDs.size()) + return false; + + for(const TModID & dependency : mod.getDependencies()) + if(!vstd::contains(resolvedModIDs, dependency)) + return false; + + for(const TModID & softDependency : mod.getSoftDependencies()) + if(vstd::contains(notResolvedModIDs, softDependency)) + return false; + + for(const TModID & conflict : mod.getConflicts()) + if(vstd::contains(resolvedModIDs, conflict)) + return false; + + for(const TModID & reverseConflict : resolvedModIDs) + if(vstd::contains(storage.getMod(reverseConflict).getConflicts(), mod.getID())) + return false; + + return true; + }; + + while(true) + { + std::set resolvedOnCurrentTreeLevel; + for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree + { + if(isResolved(storage.getMod(*it))) + { + resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration + assert(!vstd::contains(sortedValidMods, *it)); + sortedValidMods.push_back(*it); + it = modsToResolve.erase(it); + continue; + } + it++; + } + if(!resolvedOnCurrentTreeLevel.empty()) + { + resolvedModIDs.insert(resolvedOnCurrentTreeLevel.begin(), resolvedOnCurrentTreeLevel.end()); + for(const auto & it : resolvedOnCurrentTreeLevel) + notResolvedModIDs.erase(it); + continue; + } + // If there are no valid mods on the current mods tree level, no more mod can be resolved, should be ended. + break; + } + + assert(!sortedValidMods.empty()); + activeMods = sortedValidMods; + brokenMods.insert(brokenMods.end(), modsToResolve.begin(), modsToResolve.end()); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModManager.h b/lib/modding/ModManager.h new file mode 100644 index 000000000..2acdb90a6 --- /dev/null +++ b/lib/modding/ModManager.h @@ -0,0 +1,144 @@ +/* + * ModManager.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../json/JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +class ModDescription; +struct CModVersion; + +using TModID = std::string; +using TModList = std::vector; +using TModSet = std::set; + +/// Provides interface to access list of locally installed mods +class ModsState : boost::noncopyable +{ + TModList modList; + + TModList scanModsDirectory(const std::string & modDir) const; + +public: + ModsState(); + + TModList getInstalledMods() const; + double getInstalledModSizeMegabytes(const TModID & modName) const; + + uint32_t computeChecksum(const TModID & modName) const; +}; + +/// Provides interface to access or change current mod preset +class ModsPresetState : boost::noncopyable +{ + JsonNode modConfig; + + void createInitialPreset(); + void importInitialPreset(); + + const JsonNode & getActivePresetConfig() const; + +public: + ModsPresetState(); + + void setModActive(const TModID & modName, bool isActive); + + void addRootMod(const TModID & modName); + void eraseRootMod(const TModID & modName); + void removeOldMods(const TModList & modsToKeep); + + void setSettingActive(const TModID & modName, const TModID & settingName, bool isActive); + void eraseModSetting(const TModID & modName, const TModID & settingName); + + /// Returns list of all mods active in current preset. Mod order is unspecified + TModList getActiveMods() const; + + /// Returns list of currently active root mods (non-submod) + TModList getActiveRootMods() const; + + /// Returns list of all known settings (submods) for a specified mod + std::map getModSettings(const TModID & modID) const; + std::optional getValidatedChecksum(const TModID & modName) const; + void setValidatedChecksum(const TModID & modName, std::optional value); + + void saveConfigurationState() const; +}; + +/// Provides access to mod properties +class ModsStorage : boost::noncopyable +{ + std::map mods; + +public: + ModsStorage(const TModList & modsToLoad, const JsonNode & repositoryList); + + const ModDescription & getMod(const TModID & fullID) const; + + TModList getAllMods() const; +}; + +class ModDependenciesResolver : boost::noncopyable +{ + /// all currently active mods, in their load order + TModList activeMods; + + /// Mods from current preset that failed to load due to invalid dependencies + TModList brokenMods; + +public: + ModDependenciesResolver(const TModList & modsToResolve, const ModsStorage & storage); + + void tryAddMods(TModList modsToResolve, const ModsStorage & storage); + + const TModList & getActiveMods() const; + const TModList & getBrokenMods() const; +}; + +/// Provides public interface to access mod state +class DLL_LINKAGE ModManager : boost::noncopyable +{ + std::unique_ptr modsState; + std::unique_ptr modsPreset; + std::unique_ptr modsStorage; + std::unique_ptr depedencyResolver; + + void generateLoadOrder(TModList desiredModList); + void eraseMissingModsFromPreset(); + void addNewModsToPreset(); + void updatePreset(const ModDependenciesResolver & newData); + + TModList collectDependenciesRecursive(const TModID & modID) const; + + void tryEnableMod(const TModID & modList); + +public: + ModManager(const JsonNode & repositoryList); + ModManager(); + ~ModManager(); + + const ModDescription & getModDescription(const TModID & modID) const; + const TModList & getActiveMods() const; + TModList getAllMods() const; + + bool isModSettingActive(const TModID & rootModID, const TModID & modSettingID) const; + bool isModActive(const TModID & modID) const; + uint32_t computeChecksum(const TModID & modName) const; + std::optional getValidatedChecksum(const TModID & modName) const; + void setValidatedChecksum(const TModID & modName, std::optional value); + void saveConfigurationState() const; + double getInstalledModSizeMegabytes(const TModID & modName) const; + + void tryEnableMods(const TModList & modList); + void tryDisableMod(const TModID & modName); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModVerificationInfo.cpp b/lib/modding/ModVerificationInfo.cpp index db2c7370a..72104462b 100644 --- a/lib/modding/ModVerificationInfo.cpp +++ b/lib/modding/ModVerificationInfo.cpp @@ -10,9 +10,10 @@ #include "StdInc.h" #include "ModVerificationInfo.h" -#include "CModInfo.h" #include "CModHandler.h" +#include "ModDescription.h" #include "ModIncompatibility.h" +#include "ModScope.h" #include "../json/JsonNode.h" #include "../VCMI_Lib.h" @@ -68,7 +69,10 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const if(modList.count(m)) continue; - if(VLC->modh->getModInfo(m).checkModGameplayAffecting()) + if (m == ModScope::scopeBuiltin()) + continue; + + if(VLC->modh->getModInfo(m).affectsGameplay()) result[m] = ModVerificationStatus::EXCESSIVE; } @@ -77,6 +81,9 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const auto & remoteModId = infoPair.first; auto & remoteModInfo = infoPair.second; + if (remoteModId == ModScope::scopeBuiltin()) + continue; + bool modAffectsGameplay = remoteModInfo.impactsGameplay; //parent mod affects gameplay if child affects too for(const auto & subInfoPair : modList) @@ -88,8 +95,8 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const continue; } - auto & localModInfo = VLC->modh->getModInfo(remoteModId).getVerificationInfo(); - modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).checkModGameplayAffecting(); + const auto & localVersion = VLC->modh->getModInfo(remoteModId).getVersion(); + modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).affectsGameplay(); // skip it. Such mods should only be present in old saves or if mod changed and no longer affects gameplay if (!modAffectsGameplay) @@ -101,7 +108,7 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const continue; } - if(remoteModInfo.version != localModInfo.version) + if(remoteModInfo.version != localVersion) { result[remoteModId] = ModVerificationStatus::VERSION_MISMATCH; continue; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 80446c433..a187f48e6 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -45,6 +45,7 @@ #include "mapObjectConstructors/CObjectClassesHandler.h" #include "campaign/CampaignState.h" #include "IGameSettings.h" +#include "mapObjects/FlaggableMapObject.h" VCMI_LIB_NAMESPACE_BEGIN @@ -1184,7 +1185,6 @@ void RemoveBonus::applyGs(CGameState *gs) void RemoveObject::applyGs(CGameState *gs) { - CGObjectInstance *obj = gs->getObjInstance(objectID); logGlobal->debug("removing object id=%d; address=%x; name=%s", objectID, (intptr_t)obj, obj->getObjectName()); //unblock tiles @@ -1193,14 +1193,22 @@ void RemoveObject::applyGs(CGameState *gs) if (initiator.isValidPlayer()) gs->getPlayerState(initiator)->destroyedObjects.insert(objectID); + if(obj->getOwner().isValidPlayer()) + { + gs->getPlayerState(obj->getOwner())->removeOwnedObject(obj); //object removed via map event or hero got beaten + + FlaggableMapObject* flaggableObject = dynamic_cast(obj); + if(flaggableObject) + { + flaggableObject->markAsDeleted(); + } + } + if(obj->ID == Obj::HERO) //remove beaten hero { auto * beatenHero = dynamic_cast(obj); assert(beatenHero); - PlayerState * p = gs->getPlayerState(beatenHero->tempOwner); gs->map->heroesOnMap -= beatenHero; - p->removeOwnedObject(beatenHero); - auto * siegeNode = beatenHero->whereShouldBeAttachedOnSiege(gs); diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index 8e94e2c2e..9abaaed34 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -335,7 +335,8 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler) "cpuStart", "treasure", "junction", - "water" + "water", + "sealed" }; handler.serializeEnum("type", type, zoneTypes); @@ -420,6 +421,7 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler) } ZoneConnection::ZoneConnection(): + id(-1), zoneA(-1), zoneB(-1), guardStrength(0), @@ -429,6 +431,16 @@ ZoneConnection::ZoneConnection(): } +int ZoneConnection::getId() const +{ + return id; +} + +void ZoneConnection::setId(int id) +{ + this->id = id; +} + TRmgTemplateZoneId ZoneConnection::getZoneA() const { return zoneA; @@ -472,7 +484,7 @@ rmg::ERoadOption ZoneConnection::getRoadOption() const bool operator==(const ZoneConnection & l, const ZoneConnection & r) { - return l.zoneA == r.zoneA && l.zoneB == r.zoneB && l.guardStrength == r.guardStrength; + return l.id == r.id; } void ZoneConnection::serializeJson(JsonSerializeFormat & handler) @@ -591,7 +603,7 @@ const CRmgTemplate::Zones & CRmgTemplate::getZones() const const std::vector & CRmgTemplate::getConnectedZoneIds() const { - return connectedZoneIds; + return connections; } void CRmgTemplate::validate() const @@ -720,7 +732,14 @@ void CRmgTemplate::serializeJson(JsonSerializeFormat & handler) { auto connectionsData = handler.enterArray("connections"); - connectionsData.serializeStruct(connectedZoneIds); + connectionsData.serializeStruct(connections); + if(!handler.saving) + { + for(size_t i = 0; i < connections.size(); ++i) + { + connections[i].setId(i); + } + } } { @@ -842,7 +861,7 @@ void CRmgTemplate::afterLoad() } } - for(const auto & connection : connectedZoneIds) + for(const auto & connection : connections) { auto id1 = connection.getZoneA(); auto id2 = connection.getZoneB(); diff --git a/lib/rmg/CRmgTemplate.h b/lib/rmg/CRmgTemplate.h index 297f62387..649f4878d 100644 --- a/lib/rmg/CRmgTemplate.h +++ b/lib/rmg/CRmgTemplate.h @@ -28,7 +28,8 @@ enum class ETemplateZoneType CPU_START, TREASURE, JUNCTION, - WATER + WATER, + SEALED }; namespace EWaterContent // Not enum class, because it's used in method RandomMapTab::setMapGenOptions @@ -96,6 +97,8 @@ public: ZoneConnection(); + int getId() const; + void setId(int id); TRmgTemplateZoneId getZoneA() const; TRmgTemplateZoneId getZoneB() const; TRmgTemplateZoneId getOtherZoneId(TRmgTemplateZoneId id) const; @@ -107,6 +110,7 @@ public: friend bool operator==(const ZoneConnection &, const ZoneConnection &); private: + int id; TRmgTemplateZoneId zoneA; TRmgTemplateZoneId zoneB; int guardStrength; @@ -293,7 +297,7 @@ private: CPlayerCountRange players; CPlayerCountRange humanPlayers; Zones zones; - std::vector connectedZoneIds; + std::vector connections; std::set allowedWaterContent; std::unique_ptr mapSettings; diff --git a/lib/rmg/Functions.cpp b/lib/rmg/Functions.cpp index fd855e6be..2d76d2f49 100644 --- a/lib/rmg/Functions.cpp +++ b/lib/rmg/Functions.cpp @@ -24,6 +24,23 @@ VCMI_LIB_NAMESPACE_BEGIN +void replaceWithCurvedPath(rmg::Path & path, const Zone & zone, const int3 & src, bool onlyStraight) +{ + auto costFunction = rmg::Path::createCurvedCostFunction(zone.area()->getBorder()); + auto pathArea = zone.areaForRoads(); + rmg::Path curvedPath(pathArea); + curvedPath.connect(zone.freePaths().get()); + curvedPath = curvedPath.search(src, onlyStraight, costFunction); + if (curvedPath.valid()) + { + path = curvedPath; + } + else + { + logGlobal->warn("Failed to create curved path to %s", src.toString()); + } +} + rmg::Tileset collectDistantTiles(const Zone& zone, int distance) { uint32_t distanceSq = distance * distance; diff --git a/lib/rmg/Functions.h b/lib/rmg/Functions.h index 2756d0897..dfec5c3d7 100644 --- a/lib/rmg/Functions.h +++ b/lib/rmg/Functions.h @@ -34,6 +34,8 @@ public: } }; +void replaceWithCurvedPath(rmg::Path & path, const Zone & zone, const int3 & src, bool onlyStraight = true); + rmg::Tileset collectDistantTiles(const Zone & zone, int distance); int chooseRandomAppearance(vstd::RNG & generator, si32 ObjID, TerrainId terrain); diff --git a/lib/rmg/RmgPath.cpp b/lib/rmg/RmgPath.cpp index 134f08267..b36628f98 100644 --- a/lib/rmg/RmgPath.cpp +++ b/lib/rmg/RmgPath.cpp @@ -116,8 +116,7 @@ Path Path::search(const Tileset & dst, bool straight, std::functioncontains(pos)) return; - float movementCost = moveCostFunction(currentNode, pos) + currentNode.dist2d(pos); - + float movementCost = moveCostFunction(currentNode, pos); float distance = distances[currentNode] + movementCost; //we prefer to use already free paths int bestDistanceSoFar = std::numeric_limits::max(); auto it = distances.find(pos); @@ -190,4 +189,21 @@ const Area & Path::getPathArea() const return dPath; } +Path::MoveCostFunction Path::createCurvedCostFunction(const Area & border) +{ + // Capture by value to ensure the Area object persists + return [border = border](const int3& src, const int3& dst) -> float + { + // Route main roads far from border + float ret = dst.dist2d(src); + float dist = border.distanceSqr(dst); + + if(dist > 1.0f) + { + ret /= dist; + } + return ret; + }; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/RmgPath.h b/lib/rmg/RmgPath.h index a76df8cfa..7707e1abb 100644 --- a/lib/rmg/RmgPath.h +++ b/lib/rmg/RmgPath.h @@ -21,7 +21,8 @@ namespace rmg class Path { public: - const static std::function DEFAULT_MOVEMENT_FUNCTION; + using MoveCostFunction = std::function; + const static MoveCostFunction DEFAULT_MOVEMENT_FUNCTION; Path(const Area & area); Path(const Area & area, const int3 & src); @@ -42,6 +43,7 @@ public: const Area & getPathArea() const; static Path invalid(); + static MoveCostFunction createCurvedCostFunction(const Area & border); private: diff --git a/lib/rmg/Zone.cpp b/lib/rmg/Zone.cpp index 273dc226e..e40954c66 100644 --- a/lib/rmg/Zone.cpp +++ b/lib/rmg/Zone.cpp @@ -121,6 +121,11 @@ ThreadSafeProxy Zone::areaUsed() const return ThreadSafeProxy(dAreaUsed, areaMutex); } +rmg::Area Zone::areaForRoads() const +{ + return areaPossible() + freePaths(); +} + void Zone::clearTiles() { Lock lock(areaMutex); @@ -138,7 +143,7 @@ void Zone::initFreeTiles() }); dAreaPossible.assign(possibleTiles); - if(dAreaFree.empty()) + if(dAreaFree.empty() && getType() != ETemplateZoneType::SEALED) { // Fixme: This might fail fot water zone, which doesn't need to have a tile in its center of the mass dAreaPossible.erase(pos); @@ -299,7 +304,6 @@ void Zone::fractalize() logGlobal->trace("Zone %d: treasureValue %d blockDistance: %2.f, freeDistance: %2.f", getId(), treasureValue, blockDistance, freeDistance); Lock lock(areaMutex); - // FIXME: Do not access Area directly rmg::Area clearedTiles(dAreaFree); rmg::Area possibleTiles(dAreaPossible); @@ -348,6 +352,16 @@ void Zone::fractalize() tilesToIgnore.clear(); } } + else if (type == ETemplateZoneType::SEALED) + { + //Completely block all the tiles in the zone + auto tiles = areaPossible()->getTiles(); + for(const auto & t : tiles) + map.setOccupied(t, ETileType::BLOCKED); + possibleTiles.clear(); + dAreaFree.clear(); + return; + } else { // Handle special case - place Monoliths at the edge of a zone diff --git a/lib/rmg/Zone.h b/lib/rmg/Zone.h index 4ae2c7a13..5b18a6fe8 100644 --- a/lib/rmg/Zone.h +++ b/lib/rmg/Zone.h @@ -92,6 +92,8 @@ public: ThreadSafeProxy freePaths() const; ThreadSafeProxy areaUsed(); ThreadSafeProxy areaUsed() const; + + rmg::Area areaForRoads() const; void initFreeTiles(); void clearTiles(); diff --git a/lib/rmg/modificators/ConnectionsPlacer.cpp b/lib/rmg/modificators/ConnectionsPlacer.cpp index 79c4ff438..244959d1a 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.cpp +++ b/lib/rmg/modificators/ConnectionsPlacer.cpp @@ -143,7 +143,7 @@ void ConnectionsPlacer::forcePortalConnection(const rmg::ZoneConnection & connec void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & connection) { bool success = false; - auto otherZoneId = (connection.getZoneA() == zone.getId() ? connection.getZoneB() : connection.getZoneA()); + auto otherZoneId = connection.getOtherZoneId(zone.getId()); auto & otherZone = map.getZones().at(otherZoneId); bool createRoad = shouldGenerateRoad(connection); @@ -185,8 +185,8 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con return 1.f / (1.f + border.distanceSqr(d)); }; - auto ourArea = zone.areaPossible() + zone.freePaths(); - auto theirArea = otherZone->areaPossible() + otherZone->freePaths(); + auto ourArea = zone.areaForRoads(); + auto theirArea = otherZone->areaForRoads(); theirArea.add(potentialPos); rmg::Path ourPath(ourArea); rmg::Path theirPath(theirArea); @@ -278,24 +278,22 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con assert(zone.getModificator()); auto & manager = *zone.getModificator(); auto * monsterType = manager.chooseGuard(connection.getGuardStrength(), true); - + rmg::Area border(zone.area()->getBorder()); border.unite(otherZone->area()->getBorder()); - - auto costFunction = [&border](const int3 & s, const int3 & d) - { - return 1.f / (1.f + border.distanceSqr(d)); - }; - - auto ourArea = zone.areaPossible() + zone.freePaths(); - auto theirArea = otherZone->areaPossible() + otherZone->freePaths(); + + auto localCostFunction = rmg::Path::createCurvedCostFunction(zone.area()->getBorder()); + auto otherCostFunction = rmg::Path::createCurvedCostFunction(otherZone->area()->getBorder()); + + auto ourArea = zone.areaForRoads(); + auto theirArea = otherZone->areaForRoads(); theirArea.add(guardPos); rmg::Path ourPath(ourArea); rmg::Path theirPath(theirArea); ourPath.connect(zone.freePaths().get()); - ourPath = ourPath.search(guardPos, true, costFunction); + ourPath = ourPath.search(guardPos, true, localCostFunction); theirPath.connect(otherZone->freePaths().get()); - theirPath = theirPath.search(guardPos, true, costFunction); + theirPath = theirPath.search(guardPos, true, otherCostFunction); if(ourPath.valid() && theirPath.valid()) { @@ -327,10 +325,9 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con assert(otherZone->getModificator()); otherZone->getModificator()->addRoadNode(roadNode); - - assert(otherZone->getModificator()); - otherZone->getModificator()->otherSideConnection(connection); } + assert(otherZone->getModificator()); + otherZone->getModificator()->otherSideConnection(connection); success = true; } @@ -418,11 +415,14 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c if(path1.valid() && path2.valid()) { - zone.connectPath(path1); - otherZone->connectPath(path2); - manager.placeObject(rmgGate1, guarded1, true, allowRoad); managerOther.placeObject(rmgGate2, guarded2, true, allowRoad); + + replaceWithCurvedPath(path1, zone, rmgGate1.getVisitablePosition()); + replaceWithCurvedPath(path2, *otherZone, rmgGate2.getVisitablePosition()); + + zone.connectPath(path1); + otherZone->connectPath(path2); assert(otherZone->getModificator()); otherZone->getModificator()->otherSideConnection(connection); diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index c256a85c1..13b8b3000 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -344,7 +344,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg { int3 pos; auto possibleArea = searchArea; - auto cachedArea = zone.areaPossible() + zone.freePaths(); + auto cachedArea = zone.areaForRoads(); while(true) { pos = findPlaceForObject(possibleArea, obj, weightFunction, optimizer); @@ -419,6 +419,9 @@ bool ObjectManager::createMonoliths() return false; } + // Once it can be created, replace with curved path + replaceWithCurvedPath(path, zone, rmgObject.getVisitablePosition()); + zone.connectPath(path); placeObject(rmgObject, guarded, true, objInfo.createRoad); } @@ -449,6 +452,11 @@ bool ObjectManager::createRequiredObjects() logGlobal->error("Failed to fill zone %d due to lack of space", zone.getId()); return false; } + if (objInfo.createRoad) + { + // Once valid path can be created, replace with curved path + replaceWithCurvedPath(path, zone, rmgObject.getVisitablePosition()); + } zone.connectPath(path); placeObject(rmgObject, guarded, true, objInfo.createRoad); diff --git a/lib/rmg/modificators/RiverPlacer.cpp b/lib/rmg/modificators/RiverPlacer.cpp index dd4150627..9983d2011 100644 --- a/lib/rmg/modificators/RiverPlacer.cpp +++ b/lib/rmg/modificators/RiverPlacer.cpp @@ -212,7 +212,7 @@ void RiverPlacer::preprocess() { auto river = VLC->terrainTypeHandler->getById(zone.getTerrainType())->river; auto & a = neighbourZonesTiles[connectedToWaterZoneId]; - auto availableArea = zone.areaPossible() + zone.freePaths(); + auto availableArea = zone.areaForRoads(); for(const auto & tileToProcess : availableArea.getTilesVector()) { int templateId = -1; diff --git a/lib/rmg/modificators/WaterProxy.cpp b/lib/rmg/modificators/WaterProxy.cpp index 544c6f197..5172910fa 100644 --- a/lib/rmg/modificators/WaterProxy.cpp +++ b/lib/rmg/modificators/WaterProxy.cpp @@ -266,9 +266,9 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, bool createRoad, Rout rmg::Object rmgObject(*boat); rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); - auto waterAvailable = zone.areaPossible() + zone.freePaths(); + auto waterAvailable = zone.areaForRoads(); rmg::Area coast = lake.neighbourZones.at(land.getId()); //having land tiles - coast.intersect(land.areaPossible() + land.freePaths()); //having only available land tiles + coast.intersect(land.areaForRoads()); //having only available land tiles auto boardingPositions = coast.getSubarea([&waterAvailable, this](const int3 & tile) //tiles where boarding is possible { //We don't want place boat right to any land object, especiallly the zone guard @@ -332,10 +332,10 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool rmgObject.setTemplate(land.getTerrainType(), zone.getRand()); bool guarded = manager->addGuard(rmgObject, guard); - auto waterAvailable = zone.areaPossible() + zone.freePaths(); + auto waterAvailable = zone.areaForRoads(); waterAvailable.intersect(lake.area); rmg::Area coast = lake.neighbourZones.at(land.getId()); //having land tiles - coast.intersect(land.areaPossible() + land.freePaths()); //having only available land tiles + coast.intersect(land.areaForRoads()); //having only available land tiles auto boardingPositions = coast.getSubarea([&waterAvailable](const int3 & tile) //tiles where boarding is possible { rmg::Area a({tile}); diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 136d95db1..9db041b2e 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -131,12 +131,12 @@ public: if ((byteValue & 0x80) != 0) { - valueUnsigned |= (byteValue & 0x7f) << offset; + valueUnsigned |= static_cast(byteValue & 0x7f) << offset; offset += 7; } else { - valueUnsigned |= (byteValue & 0x3f) << offset; + valueUnsigned |= static_cast(byteValue & 0x3f) << offset; bool isNegative = (byteValue & 0x40) != 0; if (isNegative) return -static_cast(valueUnsigned); diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h index 5e038bbf0..a5b374f9b 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/lib/serializer/ESerializationVersion.h @@ -68,6 +68,8 @@ enum class ESerializationVersion : int32_t REMOVE_VLC_POINTERS, // 869 removed remaining pointers to VLC entities FOLDER_NAME_REWORK, // 870 - rework foldername REWARDABLE_GUARDS, // 871 - fix missing serialization of guards in rewardable objects + MARKET_TRANSLATION_FIX, // 872 - remove serialization of markets translateable strings + EVENT_OBJECTS_DELETION, //873 - allow events to remove map objects - CURRENT = REWARDABLE_GUARDS + CURRENT = EVENT_OBJECTS_DELETION }; diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index f6585d8bb..81a7f1a1b 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -259,7 +259,7 @@ std::vector BattleSpellMechanics::getAffectedStacks(const Target for(const Destination & dest : all) { - if(dest.unitValue) + if(dest.unitValue && !dest.unitValue->hasBonusOfType(BonusType::INVINCIBLE)) { //FIXME: remove and return battle::Unit stacks.insert(battle()->battleGetStackByID(dest.unitValue->unitId(), false)); diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index e1154b107..79619ac51 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -544,8 +544,8 @@ void CSpell::serializeJson(JsonSerializeFormat & handler) ///CSpell::AnimationInfo CSpell::AnimationItem::AnimationItem() : verticalPosition(VerticalPosition::TOP), - pause(0), - transparency(1) + transparency(1), + pause(0) { } diff --git a/lib/texts/Languages.h b/lib/texts/Languages.h index 9e0f87585..52b13b851 100644 --- a/lib/texts/Languages.h +++ b/lib/texts/Languages.h @@ -77,23 +77,23 @@ inline const auto & getLanguageList() { static const std::array languages { { - { "czech", "Czech", "Čeština", "CP1250", "cs", "cze", "%d.%m.%Y %H:%M:%S", EPluralForms::CZ_3 }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", "chi", "%Y-%m-%d %H:%M:%S", EPluralForms::VI_1 }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", "eng", "%Y-%m-%d %H:%M:%S", EPluralForms::EN_2 }, // English uses international date/time format here - { "finnish", "Finnish", "Suomi", "CP1252", "fi", "fin", "%d.%m.%Y %H:%M:%S", EPluralForms::EN_2, }, - { "french", "French", "Français", "CP1252", "fr", "fre", "%d/%m/%Y %H:%M:%S", EPluralForms::FR_2, }, - { "german", "German", "Deutsch", "CP1252", "de", "ger", "%d.%m.%Y %H:%M:%S", EPluralForms::EN_2, }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hun", "%Y. %m. %d. %H:%M:%S", EPluralForms::EN_2 }, - { "italian", "Italian", "Italiano", "CP1250", "it", "ita", "%d/%m/%Y %H:%M:%S", EPluralForms::EN_2 }, - { "korean", "Korean", "한국어", "CP949", "ko", "kor", "%Y-%m-%d %H:%M:%S", EPluralForms::VI_1 }, - { "polish", "Polish", "Polski", "CP1250", "pl", "pol", "%d.%m.%Y %H:%M:%S", EPluralForms::PL_3 }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", "por", "%d/%m/%Y %H:%M:%S", EPluralForms::EN_2 }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", "rus", "%d.%m.%Y %H:%M:%S", EPluralForms::UK_3 }, - { "spanish", "Spanish", "Español", "CP1252", "es", "spa", "%d/%m/%Y %H:%M:%S", EPluralForms::EN_2 }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", "swe", "%Y-%m-%d %H:%M:%S", EPluralForms::EN_2 }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "tur", "%d.%m.%Y %H:%M:%S", EPluralForms::EN_2 }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "ukr", "%d.%m.%Y %H:%M:%S", EPluralForms::UK_3 }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "vie", "%d/%m/%Y %H:%M:%S", EPluralForms::VI_1 }, // Fan translation uses special encoding + { "czech", "Czech", "Čeština", "CP1250", "cs", "cze", "%d.%m.%Y %H:%M", EPluralForms::CZ_3 }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", "chi", "%Y-%m-%d %H:%M", EPluralForms::VI_1 }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", "eng", "%Y-%m-%d %H:%M", EPluralForms::EN_2 }, // English uses international date/time format here + { "finnish", "Finnish", "Suomi", "CP1252", "fi", "fin", "%d.%m.%Y %H:%M", EPluralForms::EN_2, }, + { "french", "French", "Français", "CP1252", "fr", "fre", "%d/%m/%Y %H:%M", EPluralForms::FR_2, }, + { "german", "German", "Deutsch", "CP1252", "de", "ger", "%d.%m.%Y %H:%M", EPluralForms::EN_2, }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hun", "%Y. %m. %d. %H:%M", EPluralForms::EN_2 }, + { "italian", "Italian", "Italiano", "CP1250", "it", "ita", "%d/%m/%Y %H:%M", EPluralForms::EN_2 }, + { "korean", "Korean", "한국어", "CP949", "ko", "kor", "%Y-%m-%d %H:%M", EPluralForms::VI_1 }, + { "polish", "Polish", "Polski", "CP1250", "pl", "pol", "%d.%m.%Y %H:%M", EPluralForms::PL_3 }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", "por", "%d/%m/%Y %H:%M", EPluralForms::EN_2 }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", "rus", "%d.%m.%Y %H:%M", EPluralForms::UK_3 }, + { "spanish", "Spanish", "Español", "CP1252", "es", "spa", "%d/%m/%Y %H:%M", EPluralForms::EN_2 }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", "swe", "%Y-%m-%d %H:%M", EPluralForms::EN_2 }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "tur", "%d.%m.%Y %H:%M", EPluralForms::EN_2 }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "ukr", "%d.%m.%Y %H:%M", EPluralForms::UK_3 }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "vie", "%d/%m/%Y %H:%M", EPluralForms::VI_1 }, // Fan translation uses special encoding } }; static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); diff --git a/lib/texts/MetaString.cpp b/lib/texts/MetaString.cpp index e77fc65e6..0bb7304ec 100644 --- a/lib/texts/MetaString.cpp +++ b/lib/texts/MetaString.cpp @@ -393,11 +393,16 @@ void MetaString::replaceName(const FactionID & id) replaceTextID(id.toEntity(VLC)->getNameTextID()); } -void MetaString::replaceName(const MapObjectID& id) +void MetaString::replaceName(const MapObjectID & id) { replaceTextID(VLC->objtypeh->getObjectName(id, 0)); } +void MetaString::replaceName(const MapObjectID & id, const MapObjectSubID & subId) +{ + replaceTextID(VLC->objtypeh->getObjectName(id, subId)); +} + void MetaString::replaceName(const PlayerColor & id) { replaceTextID(TextIdentifier("vcmi.capitalColors", id.getNum()).get()); diff --git a/lib/texts/MetaString.h b/lib/texts/MetaString.h index d55cc279e..e13b55709 100644 --- a/lib/texts/MetaString.h +++ b/lib/texts/MetaString.h @@ -99,7 +99,8 @@ public: void replaceName(const ArtifactID & id); void replaceName(const FactionID& id); - void replaceName(const MapObjectID& id); + void replaceName(const MapObjectID & id); + void replaceName(const MapObjectID & id, const MapObjectSubID & subId); void replaceName(const PlayerColor& id); void replaceName(const SecondarySkill& id); void replaceName(const SpellID& id); diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index e9f7ad6cf..dd1f49493 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -34,6 +34,8 @@ set(editor_SRCS inspector/messagewidget.cpp inspector/rewardswidget.cpp inspector/questwidget.cpp + inspector/heroartifactswidget.cpp + inspector/artifactwidget.cpp inspector/heroskillswidget.cpp inspector/herospellwidget.cpp inspector/PickObjectDelegate.cpp @@ -76,6 +78,8 @@ set(editor_HEADERS inspector/messagewidget.h inspector/rewardswidget.h inspector/questwidget.h + inspector/heroartifactswidget.h + inspector/artifactwidget.h inspector/heroskillswidget.h inspector/herospellwidget.h inspector/PickObjectDelegate.h @@ -108,6 +112,8 @@ set(editor_FORMS inspector/messagewidget.ui inspector/rewardswidget.ui inspector/questwidget.ui + inspector/heroartifactswidget.ui + inspector/artifactwidget.ui inspector/heroskillswidget.ui inspector/herospellwidget.ui inspector/portraitwidget.ui diff --git a/mapeditor/inspector/artifactwidget.cpp b/mapeditor/inspector/artifactwidget.cpp new file mode 100644 index 000000000..3723eb428 --- /dev/null +++ b/mapeditor/inspector/artifactwidget.cpp @@ -0,0 +1,63 @@ +/* + * herosspellwidget.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "artifactwidget.h" +#include "ui_artifactwidget.h" +#include "inspector.h" +#include "../../lib/ArtifactUtils.h" +#include "../../lib/constants/StringConstants.h" + +ArtifactWidget::ArtifactWidget(CArtifactFittingSet & fittingSet, QWidget * parent) : + QDialog(parent), + ui(new Ui::ArtifactWidget), + fittingSet(fittingSet) +{ + ui->setupUi(this); + + connect(ui->saveButton, &QPushButton::clicked, this, [this]() + { + emit saveArtifact(ui->artifact->currentData().toInt(), ArtifactPosition(ui->possiblePositions->currentData().toInt())); + close(); + }); + connect(ui->cancelButton, &QPushButton::clicked, this, &ArtifactWidget::close); + connect(ui->possiblePositions, static_cast (&QComboBox::currentIndexChanged), this, &ArtifactWidget::fillArtifacts); + + std::vector possiblePositions; + for(const auto & slot : ArtifactUtils::allWornSlots()) + { + if(fittingSet.isPositionFree(slot)) + { + ui->possiblePositions->addItem(QString::fromStdString(NArtifactPosition::namesHero[slot.num]), slot.num); + } + } + ui->possiblePositions->addItem(QString::fromStdString(NArtifactPosition::backpack), ArtifactPosition::BACKPACK_START); + fillArtifacts(); + + +} + +void ArtifactWidget::fillArtifacts() +{ + ui->artifact->clear(); + auto currentSlot = ui->possiblePositions->currentData().toInt(); + for (const auto& art : VLC->arth->getDefaultAllowed()) + { + auto artifact = art.toArtifact(); + // forbid spell scroll for now as require special handling + if (artifact->canBePutAt(&fittingSet, currentSlot, true) && artifact->getId() != ArtifactID::SPELL_SCROLL) { + ui->artifact->addItem(QString::fromStdString(artifact->getNameTranslated()), QVariant::fromValue(artifact->getIndex())); + } + } +} + +ArtifactWidget::~ArtifactWidget() +{ + delete ui; +} diff --git a/mapeditor/inspector/artifactwidget.h b/mapeditor/inspector/artifactwidget.h new file mode 100644 index 000000000..d2a474240 --- /dev/null +++ b/mapeditor/inspector/artifactwidget.h @@ -0,0 +1,35 @@ +/* + * ArtifactWidget.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include +#include "../../lib/mapObjects/CGHeroInstance.h" + +namespace Ui { +class ArtifactWidget; +} + +class ArtifactWidget : public QDialog +{ + Q_OBJECT + +public: + explicit ArtifactWidget(CArtifactFittingSet & fittingSet, QWidget * parent = nullptr); + ~ArtifactWidget(); + +signals: + void saveArtifact(int32_t artifactIndex, ArtifactPosition slot); + private slots: + void fillArtifacts(); + +private: + Ui::ArtifactWidget * ui; + CArtifactFittingSet & fittingSet; +}; diff --git a/mapeditor/inspector/artifactwidget.ui b/mapeditor/inspector/artifactwidget.ui new file mode 100644 index 000000000..e9c62b0ce --- /dev/null +++ b/mapeditor/inspector/artifactwidget.ui @@ -0,0 +1,92 @@ + + + ArtifactWidget + + + Qt::WindowModal + + + + 0 + 0 + 400 + 150 + + + + + 400 + 150 + + + + + 400 + 150 + + + + Artifact + + + + + 10 + 10 + 381 + 80 + + + + + + + Artifact + + + + + + + + + + + + + Equip where: + + + + + + + + + 190 + 100 + 93 + 28 + + + + Save + + + + + + 290 + 100 + 93 + 28 + + + + Cancel + + + + + + diff --git a/mapeditor/inspector/heroartifactswidget.cpp b/mapeditor/inspector/heroartifactswidget.cpp new file mode 100644 index 000000000..2c1c9d687 --- /dev/null +++ b/mapeditor/inspector/heroartifactswidget.cpp @@ -0,0 +1,144 @@ +/* + * herosspellwidget.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "artifactwidget.h" +#include "heroartifactswidget.h" +#include "ui_heroartifactswidget.h" +#include "inspector.h" +#include "mapeditorroles.h" +#include "../../lib/ArtifactUtils.h" +#include "../../lib/constants/StringConstants.h" + +HeroArtifactsWidget::HeroArtifactsWidget(CGHeroInstance & h, QWidget * parent) : + QDialog(parent), + ui(new Ui::HeroArtifactsWidget), + hero(h), + fittingSet(CArtifactFittingSet(h)) +{ + ui->setupUi(this); +} + +HeroArtifactsWidget::~HeroArtifactsWidget() +{ + delete ui; +} + +void HeroArtifactsWidget::on_addButton_clicked() +{ + ArtifactWidget artifactWidget{ fittingSet, this }; + connect(&artifactWidget, &ArtifactWidget::saveArtifact, this, &HeroArtifactsWidget::onSaveArtifact); + artifactWidget.exec(); +} + +void HeroArtifactsWidget::on_removeButton_clicked() +{ + auto row = ui->artifacts->currentRow(); + if (row == -1) + { + return; + } + + auto slot = ui->artifacts->item(row, Column::SLOT)->data(MapEditorRoles::ArtifactSlotRole).toInt(); + fittingSet.removeArtifact(ArtifactPosition(slot)); + ui->artifacts->removeRow(row); +} + +void HeroArtifactsWidget::onSaveArtifact(int32_t artifactIndex, ArtifactPosition slot) +{ + auto artifact = ArtifactUtils::createArtifact(VLC->arth->getByIndex(artifactIndex)->getId()); + fittingSet.putArtifact(slot, artifact); + addArtifactToTable(artifactIndex, slot); +} + +void HeroArtifactsWidget::addArtifactToTable(int32_t artifactIndex, ArtifactPosition slot) +{ + auto artifact = VLC->arth->getByIndex(artifactIndex); + auto * itemArtifact = new QTableWidgetItem; + itemArtifact->setText(QString::fromStdString(artifact->getNameTranslated())); + itemArtifact->setData(MapEditorRoles::ArtifactIDRole, QVariant::fromValue(artifact->getIndex())); + + auto * itemSlot = new QTableWidgetItem; + auto slotText = ArtifactUtils::isSlotBackpack(slot) ? NArtifactPosition::backpack : NArtifactPosition::namesHero[slot.num]; + itemSlot->setData(MapEditorRoles::ArtifactSlotRole, QVariant::fromValue(slot.num)); + itemSlot->setText(QString::fromStdString(slotText)); + + ui->artifacts->insertRow(ui->artifacts->rowCount()); + ui->artifacts->setItem(ui->artifacts->rowCount() - 1, Column::ARTIFACT, itemArtifact); + ui->artifacts->setItem(ui->artifacts->rowCount() - 1, Column::SLOT, itemSlot); +} + +void HeroArtifactsWidget::obtainData() +{ + std::vector combinedArtifactsParts; + for (const auto & [artPosition, artSlotInfo] : fittingSet.artifactsWorn) + { + addArtifactToTable(VLC->arth->getById(artSlotInfo.artifact->getTypeId())->getIndex(), artPosition); + } + for (const auto & art : hero.artifactsInBackpack) + { + addArtifactToTable(VLC->arth->getById(art.artifact->getTypeId())->getIndex(), ArtifactPosition::BACKPACK_START); + } +} + +void HeroArtifactsWidget::commitChanges() +{ + while(!hero.artifactsWorn.empty()) + { + hero.removeArtifact(hero.artifactsWorn.begin()->first); + } + + while(!hero.artifactsInBackpack.empty()) + { + hero.removeArtifact(ArtifactPosition::BACKPACK_START + static_cast(hero.artifactsInBackpack.size()) - 1); + } + + for(const auto & [artPosition, artSlotInfo] : fittingSet.artifactsWorn) + { + hero.putArtifact(artPosition, artSlotInfo.artifact); + } + + for(const auto & art : fittingSet.artifactsInBackpack) + { + hero.putArtifact(ArtifactPosition::BACKPACK_START + static_cast(hero.artifactsInBackpack.size()), art.artifact); + } +} + +HeroArtifactsDelegate::HeroArtifactsDelegate(CGHeroInstance & h) : QStyledItemDelegate(), hero(h) +{ +} + +QWidget * HeroArtifactsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + return new HeroArtifactsWidget(hero, parent); +} + +void HeroArtifactsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void HeroArtifactsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->commitChanges(); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/heroartifactswidget.h b/mapeditor/inspector/heroartifactswidget.h new file mode 100644 index 000000000..807a37ce5 --- /dev/null +++ b/mapeditor/inspector/heroartifactswidget.h @@ -0,0 +1,65 @@ +/* + * heroartifactswidget.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include +#include "../../lib/mapObjects/CGHeroInstance.h" + +namespace Ui { +class HeroArtifactsWidget; +} + +class HeroArtifactsWidget : public QDialog +{ + Q_OBJECT + +public: + explicit HeroArtifactsWidget(CGHeroInstance &, QWidget *parent = nullptr); + ~HeroArtifactsWidget(); + + void obtainData(); + void commitChanges(); + +private slots: + void onSaveArtifact(int32_t artifactIndex, ArtifactPosition slot); + + void on_addButton_clicked(); + + void on_removeButton_clicked(); + +private: + enum Column + { + SLOT, ARTIFACT + }; + Ui::HeroArtifactsWidget * ui; + + CGHeroInstance & hero; + CArtifactFittingSet fittingSet; + + void addArtifactToTable(int32_t artifactIndex, ArtifactPosition slot); + +}; + +class HeroArtifactsDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + HeroArtifactsDelegate(CGHeroInstance &); + + QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + void setEditorData(QWidget * editor, const QModelIndex & index) const override; + void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; + +private: + CGHeroInstance & hero; +}; diff --git a/mapeditor/inspector/heroartifactswidget.ui b/mapeditor/inspector/heroartifactswidget.ui new file mode 100644 index 000000000..c3d326618 --- /dev/null +++ b/mapeditor/inspector/heroartifactswidget.ui @@ -0,0 +1,144 @@ + + + HeroArtifactsWidget + + + Qt::NonModal + + + + 0 + 0 + 480 + 635 + + + + + 0 + 0 + + + + + 480 + 480 + + + + Artifacts + + + true + + + + 10 + + + 5 + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + + 90 + 0 + + + + Add + + + + + + + true + + + + 90 + 0 + + + + Remove + + + + + + + + + true + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + 120 + + + true + + + false + + + 26 + + + + Slot + + + + + Artifact + + + + + + + + + diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index ef1696d73..3201e46d8 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -28,6 +28,7 @@ #include "messagewidget.h" #include "rewardswidget.h" #include "questwidget.h" +#include "heroartifactswidget.h" #include "heroskillswidget.h" #include "herospellwidget.h" #include "portraitwidget.h" @@ -91,6 +92,20 @@ void Initializer::initialize(CGDwelling * o) if(!o) return; o->tempOwner = defaultPlayer; + + if(o->ID == Obj::RANDOM_DWELLING || o->ID == Obj::RANDOM_DWELLING_LVL || o->ID == Obj::RANDOM_DWELLING_FACTION) + { + o->randomizationInfo = CGDwellingRandomizationInfo(); + if(o->ID == Obj::RANDOM_DWELLING_LVL) + { + o->randomizationInfo->minLevel = o->subID; + o->randomizationInfo->maxLevel = o->subID; + } + if(o->ID == Obj::RANDOM_DWELLING_FACTION) + { + o->randomizationInfo->allowedFactions.insert(FactionID(o->subID)); + } + } } void Initializer::initialize(CGGarrison * o) @@ -319,6 +334,7 @@ void Inspector::updateProperties(CGHeroInstance * o) auto * delegate = new HeroSkillsDelegate(*o); addProperty("Skills", PropertyEditorPlaceholder(), delegate, false); addProperty("Spells", PropertyEditorPlaceholder(), new HeroSpellDelegate(*o), false); + addProperty("Artifacts", PropertyEditorPlaceholder(), new HeroArtifactsDelegate(*o), false); if(o->getHeroTypeID().hasValue() || o->ID == Obj::PRISON) { //Hero type diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 7b50a9e09..e1f6abfac 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -23,7 +23,7 @@ #include "../lib/mapping/CMapEditManager.h" #include "../lib/mapping/ObstacleProxy.h" #include "../lib/modding/CModHandler.h" -#include "../lib/modding/CModInfo.h" +#include "../lib/modding/ModDescription.h" #include "../lib/TerrainHandler.h" #include "../lib/CSkillHandler.h" #include "../lib/spells/CSpellHandler.h" @@ -640,9 +640,19 @@ ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map) continue; extractEntityMod(spellID.toEntity(VLC)); } + + for(const auto & [_, slotInfo] : hero->artifactsWorn) + { + extractEntityMod(slotInfo.artifact->getTypeId().toEntity(VLC)); + } + + for(const auto & art : hero->artifactsInBackpack) + { + extractEntityMod(art.artifact->getTypeId().toEntity(VLC)); + } } } - //TODO: terrains, artifacts? + //TODO: terrains? return result; } diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index 7b8a246eb..55235f05f 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -13,9 +13,8 @@ #include "maphandler.h" #include "mapview.h" -#include "../lib/modding/CModInfo.h" - VCMI_LIB_NAMESPACE_BEGIN +struct ModVerificationInfo; using ModCompatibilityInfo = std::map; class EditorObstaclePlacer; VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/mapeditorroles.h b/mapeditor/mapeditorroles.h index 3564ad598..ed50a0be9 100644 --- a/mapeditor/mapeditorroles.h +++ b/mapeditor/mapeditorroles.h @@ -16,5 +16,8 @@ enum MapEditorRoles TownEventRole = Qt::UserRole + 1, PlayerIDRole, BuildingIDRole, - SpellIDRole + SpellIDRole, + ObjectInstanceIDRole, + ArtifactIDRole, + ArtifactSlotRole, }; diff --git a/mapeditor/mapsettings/abstractsettings.h b/mapeditor/mapsettings/abstractsettings.h index 53ab653e5..bf729be06 100644 --- a/mapeditor/mapsettings/abstractsettings.h +++ b/mapeditor/mapsettings/abstractsettings.h @@ -14,6 +14,8 @@ #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGHeroInstance.h" +Q_DECLARE_METATYPE(int3) + //parses date for lose condition (1m 1w 1d) int expiredDate(const QString & date); QString expiredDate(int date); diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp index f7c2bbc76..a74cf5a21 100644 --- a/mapeditor/mapsettings/eventsettings.cpp +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -55,6 +55,28 @@ TResources resourcesFromVariant(const QVariant & v) return TResources(vJson); } +QVariant toVariant(std::vector objects) +{ + QVariantList result; + for(auto obj : objects) + { + result.push_back(QVariant::fromValue(obj.num)); + } + return result; +} + +std::vector deletedObjectsIdsFromVariant(const QVariant & v) +{ + std::vector result; + for(auto idAsVariant : v.toList()) + { + auto id = idAsVariant.value(); + result.push_back(ObjectInstanceID(id)); + } + + return result; +} + QVariant toVariant(const CMapEvent & event) { QVariantMap result; @@ -66,6 +88,7 @@ QVariant toVariant(const CMapEvent & event) result["firstOccurrence"] = QVariant::fromValue(event.firstOccurrence); result["nextOccurrence"] = QVariant::fromValue(event.nextOccurrence); result["resources"] = toVariant(event.resources); + result["deletedObjectsInstances"] = toVariant(event.deletedObjectsInstances); return QVariant(result); } @@ -81,6 +104,7 @@ CMapEvent eventFromVariant(CMapHeader & mapHeader, const QVariant & variant) result.firstOccurrence = v.value("firstOccurrence").toInt(); result.nextOccurrence = v.value("nextOccurrence").toInt(); result.resources = resourcesFromVariant(v.value("resources")); + result.deletedObjectsInstances = deletedObjectsIdsFromVariant(v.value("deletedObjectsInstances")); return result; } @@ -137,6 +161,6 @@ void EventSettings::on_timedEventRemove_clicked() void EventSettings::on_eventsList_itemActivated(QListWidgetItem *item) { - new TimedEvent(item, parentWidget()); + new TimedEvent(*controller, item, parentWidget()); } diff --git a/mapeditor/mapsettings/modsettings.cpp b/mapeditor/mapsettings/modsettings.cpp index 330cb9c82..931566fb7 100644 --- a/mapeditor/mapsettings/modsettings.cpp +++ b/mapeditor/mapsettings/modsettings.cpp @@ -11,9 +11,9 @@ #include "modsettings.h" #include "ui_modsettings.h" #include "../mapcontroller.h" +#include "../../lib/modding/ModDescription.h" #include "../../lib/modding/CModHandler.h" #include "../../lib/mapping/CMapService.h" -#include "../../lib/modding/CModInfo.h" void traverseNode(QTreeWidgetItem * item, std::function action) { @@ -45,12 +45,12 @@ void ModSettings::initialize(MapController & c) QSet modsToProcess; ui->treeMods->blockSignals(true); - auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo) + auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const ModDescription & modInfo) { - auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.getVerificationInfo().name), QString::fromStdString(modInfo.getVerificationInfo().version.toString())}); - item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier))); + auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.getName()), QString::fromStdString(modInfo.getVersion().toString())}); + item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.getID()))); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(0, controller->map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); + item->setCheckState(0, controller->map()->mods.count(modInfo.getID()) ? Qt::Checked : Qt::Unchecked); //set parent check if(parent && item->checkState(0) == Qt::Checked) parent->setCheckState(0, Qt::Checked); diff --git a/mapeditor/mapsettings/timedevent.cpp b/mapeditor/mapsettings/timedevent.cpp index 772eea5c3..39b983295 100644 --- a/mapeditor/mapsettings/timedevent.cpp +++ b/mapeditor/mapsettings/timedevent.cpp @@ -11,13 +11,15 @@ #include "timedevent.h" #include "ui_timedevent.h" #include "eventsettings.h" +#include "../mapeditorroles.h" #include "../../lib/constants/EntityIdentifiers.h" #include "../../lib/constants/StringConstants.h" -TimedEvent::TimedEvent(QListWidgetItem * t, QWidget *parent) : +TimedEvent::TimedEvent(MapController & c, QListWidgetItem * t, QWidget *parent) : + controller(c), QDialog(parent), - target(t), - ui(new Ui::TimedEvent) + ui(new Ui::TimedEvent), + target(t) { ui->setupUi(this); @@ -51,7 +53,14 @@ TimedEvent::TimedEvent(QListWidgetItem * t, QWidget *parent) : nval->setFlags(nval->flags() | Qt::ItemIsEditable); ui->resources->setItem(i, 1, nval); } - + auto deletedObjectInstances = params.value("deletedObjectsInstances").toList(); + for(auto const & idAsVariant : deletedObjectInstances) + { + auto id = ObjectInstanceID(idAsVariant.toInt()); + auto obj = controller.map()->objects[id]; + if(obj) + insertObjectToDelete(obj); + } show(); } @@ -89,10 +98,63 @@ void TimedEvent::on_TimedEvent_finished(int result) } descriptor["resources"] = res; + QVariantList deletedObjects; + for(int i = 0; i < ui->deletedObjects->count(); ++i) + { + auto const & item = ui->deletedObjects->item(i); + auto data = item->data(MapEditorRoles::ObjectInstanceIDRole); + auto id = ObjectInstanceID(data.value()); + deletedObjects.push_back(QVariant::fromValue(id.num)); + } + descriptor["deletedObjectsInstances"] = QVariant::fromValue(deletedObjects); + target->setData(Qt::UserRole, descriptor); target->setText(ui->eventNameText->text()); } +void TimedEvent::on_addObjectToDelete_clicked() +{ + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.highlight(); + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &TimedEvent::onObjectPicked); + } + hide(); + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->hide(); +} + +void TimedEvent::on_removeObjectToDelete_clicked() +{ + delete ui->deletedObjects->takeItem(ui->deletedObjects->currentRow()); +} + +void TimedEvent::onObjectPicked(const CGObjectInstance * obj) +{ + show(); + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->show(); + + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &TimedEvent::onObjectPicked); + } + + if(!obj) + return; + insertObjectToDelete(obj); +} + +void TimedEvent::insertObjectToDelete(const CGObjectInstance * obj) +{ + QString objectLabel = QString("%1, x: %2, y: %3, z: %4").arg(QString::fromStdString(obj->getObjectName())).arg(obj->pos.x).arg(obj->pos.y).arg(obj->pos.z); + auto * item = new QListWidgetItem(objectLabel); + item->setData(MapEditorRoles::ObjectInstanceIDRole, QVariant::fromValue(obj->id.num)); + ui->deletedObjects->addItem(item); +} void TimedEvent::on_pushButton_clicked() { diff --git a/mapeditor/mapsettings/timedevent.h b/mapeditor/mapsettings/timedevent.h index 5aab63fc2..d15ccfd02 100644 --- a/mapeditor/mapsettings/timedevent.h +++ b/mapeditor/mapsettings/timedevent.h @@ -11,6 +11,8 @@ #include +#include "mapcontroller.h" + namespace Ui { class TimedEvent; } @@ -20,18 +22,23 @@ class TimedEvent : public QDialog Q_OBJECT public: - explicit TimedEvent(QListWidgetItem *, QWidget *parent = nullptr); + explicit TimedEvent(MapController & map, QListWidgetItem *, QWidget * parent = nullptr); ~TimedEvent(); private slots: void on_TimedEvent_finished(int result); + void on_addObjectToDelete_clicked(); + void on_removeObjectToDelete_clicked(); + void onObjectPicked(const CGObjectInstance * obj); + void insertObjectToDelete(const CGObjectInstance * obj); void on_pushButton_clicked(); - void on_resources_itemDoubleClicked(QTableWidgetItem *item); + void on_resources_itemDoubleClicked(QTableWidgetItem * item); private: - Ui::TimedEvent *ui; + MapController & controller; + Ui::TimedEvent * ui; QListWidgetItem * target; }; diff --git a/mapeditor/mapsettings/timedevent.ui b/mapeditor/mapsettings/timedevent.ui index 104dd16b5..59ae81f48 100644 --- a/mapeditor/mapsettings/timedevent.ui +++ b/mapeditor/mapsettings/timedevent.ui @@ -9,8 +9,8 @@ 0 0 - 620 - 371 + 730 + 422 @@ -201,6 +201,34 @@ + + + + + + + + Objects to delete + + + + + + + Add + + + + + + + Remove + + + + + + diff --git a/mapeditor/translation/chinese.ts b/mapeditor/translation/chinese.ts index bd1064557..1f0647cee 100644 --- a/mapeditor/translation/chinese.ts +++ b/mapeditor/translation/chinese.ts @@ -937,14 +937,14 @@ No patrol - + 无巡逻 %n tile(s) - - + + %n格 diff --git a/mapeditor/translation/czech.ts b/mapeditor/translation/czech.ts index 448af1e3d..204b983c6 100644 --- a/mapeditor/translation/czech.ts +++ b/mapeditor/translation/czech.ts @@ -937,16 +937,16 @@ No patrol - + Bez hlídky %n tile(s) - - - - + + %n pole + %n pole + %n polí diff --git a/mapeditor/translation/portuguese.ts b/mapeditor/translation/portuguese.ts index 51968dcaa..488062ea3 100644 --- a/mapeditor/translation/portuguese.ts +++ b/mapeditor/translation/portuguese.ts @@ -72,7 +72,7 @@ Author contact (e.g. email) - Contato do autor (ex.: e-mail) + Contato do autor (ex: e-mail) @@ -87,7 +87,7 @@ Limit maximum heroes level - Limite máximo do nível dos heróis + Limitar nível máximo dos heróis @@ -207,7 +207,7 @@ No special loss - Sem perda especial + Sem derrota especial @@ -371,7 +371,7 @@ Cut - Cortar + Recortar @@ -516,12 +516,12 @@ Lock - Travar + Bloquear Lock objects on map to avoid unnecessary changes - Travar objetos no mapa para evitar alterações desnecessárias + Bloquear objetos no mapa para evitar alterações desnecessárias @@ -566,7 +566,7 @@ Zoom reset - Redefinir do zoom + Redefinir zoom @@ -831,7 +831,7 @@ Random faction - Fação aleatória + Facção aleatória @@ -911,7 +911,7 @@ Compliant - Conformista + Complacente @@ -937,15 +937,15 @@ No patrol - + Sem patrulha %n tile(s) - - - + + %n bloco + %n blocos @@ -1025,7 +1025,7 @@ Primary skills - Habilidades principais + Habilidades primárias @@ -1241,7 +1241,7 @@ Primary skills - Habilidades principais + Habilidades primárias @@ -1397,7 +1397,7 @@ Tavern rumors - Boatos da taverna + Rumores da taverna @@ -1470,7 +1470,7 @@ qty - quantidade + qtd diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index 17c1d2dcc..a2d965e49 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -16,7 +16,7 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapObjects/MapObjects.h" #include "../lib/modding/CModHandler.h" -#include "../lib/modding/CModInfo.h" +#include "../lib/modding/ModDescription.h" #include "../lib/spells/CSpellHandler.h" Validator::Validator(const CMap * map, QWidget *parent) : diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 4a860e869..e73ef1e88 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -787,7 +787,7 @@ bool CGameHandler::removeObject(const CGObjectInstance * obj, const PlayerColor ro.initiator = initiator; sendAndApply(ro); - checkVictoryLossConditionsForAll(); //eg if monster escaped (removing objs after battle is done dircetly by endBattle, not this function) + checkVictoryLossConditionsForAll(); //e.g. if monster escaped (removing objs after battle is done directly by endBattle, not this function) return true; } @@ -886,7 +886,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme return complainRet("Cannot disembark hero, tile is blocked!"); if(distance(h->pos, dst) >= 1.5 && movementMode == EMovementMode::STANDARD) - return complainRet("Tiles are not neighboring!"); + return complainRet("Tiles " + h->pos.toString()+ " and "+ dst.toString() +" are not neighboring!"); if(h->inTownGarrison) return complainRet("Can not move garrisoned hero!"); @@ -1473,7 +1473,7 @@ bool CGameHandler::isPlayerOwns(CPackForServer * pack, ObjectInstanceID id) void CGameHandler::throwNotAllowedAction(CPackForServer * pack) { if(pack->c) - playerMessages->sendSystemMessage(pack->c, "You are not allowed to perform this action!"); + playerMessages->sendSystemMessage(pack->c, MetaString::createFromTextID("vcmi.server.errors.notAllowed")); logNetwork->error("Player is not allowed to perform this action!"); throw ExceptionNotAllowedAction(); @@ -1481,12 +1481,13 @@ void CGameHandler::throwNotAllowedAction(CPackForServer * pack) void CGameHandler::wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer) { - std::ostringstream oss; - oss << "You were identified as player " << pack->player << " while expecting " << expectedplayer; - logNetwork->error(oss.str()); + auto str = MetaString::createFromTextID("vcmi.server.errors.wrongIdentified"); + str.appendName(pack->player); + str.appendName(expectedplayer); + logNetwork->error(str.toString()); if(pack->c) - playerMessages->sendSystemMessage(pack->c, oss.str()); + playerMessages->sendSystemMessage(pack->c, str); } void CGameHandler::throwIfWrongOwner(CPackForServer * pack, ObjectInstanceID id) @@ -1569,16 +1570,18 @@ bool CGameHandler::load(const std::string & filename) catch(const ModIncompatibility & e) { logGlobal->error("Failed to load game: %s", e.what()); - std::string errorMsg; + MetaString errorMsg; if(!e.whatMissing().empty()) { - errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; - errorMsg += e.whatMissing(); + errorMsg.appendTextID("vcmi.server.errors.modsToEnable"); + errorMsg.appendRawString("\n"); + errorMsg.appendRawString(e.whatMissing()); } if(!e.whatExcessive().empty()) { - errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; - errorMsg += e.whatExcessive(); + errorMsg.appendTextID("vcmi.server.errors.modsToDisable"); + errorMsg.appendRawString("\n"); + errorMsg.appendRawString(e.whatExcessive()); } lobby->announceMessage(errorMsg); return false; @@ -1589,14 +1592,17 @@ bool CGameHandler::load(const std::string & filename) MetaString errorMsg; errorMsg.appendTextID("vcmi.server.errors.unknownEntity"); errorMsg.replaceRawString(e.identifierName); - lobby->announceMessage(errorMsg.toString());//FIXME: should be localized on client side + lobby->announceMessage(errorMsg); return false; } catch(const std::exception & e) { logGlobal->error("Failed to load game: %s", e.what()); - lobby->announceMessage(std::string("Failed to load game: ") + e.what()); + auto str = MetaString::createFromTextID("vcmi.broadcast.failedLoadGame"); + str.appendRawString(": "); + str.appendRawString(e.what()); + lobby->announceMessage(str); return false; } gs->preInit(VLC, this); @@ -3264,7 +3270,11 @@ bool CGameHandler::queryReply(QueryID qid, std::optional answer, Player bool CGameHandler::complain(const std::string &problem) { #ifndef ENABLE_GOLDMASTER - playerMessages->broadcastSystemMessage("Server encountered a problem: " + problem); + MetaString str; + str.appendTextID("vcmi.broadcast.serverProblem"); + str.appendRawString(": "); + str.appendRawString(problem); + playerMessages->broadcastSystemMessage(str); #endif logGlobal->error(problem); return true; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 54fa8a25f..2b0a27730 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -300,7 +300,7 @@ void CVCMIServer::onDisconnected(const std::shared_ptr & con std::shared_ptr c = findConnection(connection); // player may have already disconnected via clientDisconnected call - if (c && gh && getState() == EServerState::GAMEPLAY) + if (c) { LobbyClientDisconnected lcd; lcd.c = c; @@ -1016,7 +1016,7 @@ void CVCMIServer::multiplayerWelcomeMessage() if(humanPlayer < 2) // Singleplayer return; - gh->playerMessages->broadcastSystemMessage("Use '!help' to list available commands"); + gh->playerMessages->broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.command")); for (const auto & pi : si->playerInfos) if(!pi.second.handicap.startBonus.empty() || pi.second.handicap.percentIncome != 100 || pi.second.handicap.percentGrowth != 100) diff --git a/server/GlobalLobbyProcessor.cpp b/server/GlobalLobbyProcessor.cpp index 15fd376f7..418fad4da 100644 --- a/server/GlobalLobbyProcessor.cpp +++ b/server/GlobalLobbyProcessor.cpp @@ -15,7 +15,8 @@ #include "../lib/json/JsonUtils.h" #include "../lib/VCMI_Lib.h" #include "../lib/modding/CModHandler.h" -#include "../lib/modding/CModInfo.h" +#include "../lib/modding/ModDescription.h" +#include "../lib/modding/ModVerificationInfo.h" GlobalLobbyProcessor::GlobalLobbyProcessor(CVCMIServer & owner) : owner(owner) @@ -161,7 +162,7 @@ JsonNode GlobalLobbyProcessor::getHostModList() const for (auto const & modName : VLC->modh->getActiveMods()) { - if(VLC->modh->getModInfo(modName).checkModGameplayAffecting()) + if(VLC->modh->getModInfo(modName).affectsGameplay()) info[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 6855fd6c9..767cfc065 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -917,7 +917,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const handleAttackBeforeCasting(battle, ranged, attacker, defender); // If the attacker or defender is not alive before the attack action, the action should be skipped. - if((attacker && !attacker->alive()) || (defender && !defender->alive())) + if((!attacker->alive()) || (defender && !defender->alive())) return; FireShieldInfo fireShield; diff --git a/server/processors/NewTurnProcessor.cpp b/server/processors/NewTurnProcessor.cpp index 27ae99bc4..abd130793 100644 --- a/server/processors/NewTurnProcessor.cpp +++ b/server/processors/NewTurnProcessor.cpp @@ -18,6 +18,7 @@ #include "../../lib/IGameSettings.h" #include "../../lib/StartInfo.h" #include "../../lib/TerrainHandler.h" +#include "../../lib/constants/StringConstants.h" #include "../../lib/entities/building/CBuilding.h" #include "../../lib/entities/faction/CTownHandler.h" #include "../../lib/gameState/CGameState.h" @@ -60,6 +61,14 @@ void NewTurnProcessor::handleTimeEvents(PlayerColor color) if (event.resources[i]) iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]); } + + //remove objects specified by event + for(const ObjectInstanceID objectIdToRemove : event.deletedObjectsInstances) + { + auto objectInstance = gameHandler->getObj(objectIdToRemove, false); + if(objectInstance != nullptr) + gameHandler->removeObject(objectInstance, PlayerColor::NEUTRAL); + } gameHandler->sendAndApply(iw); //show dialog } } @@ -237,6 +246,28 @@ ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool ne for (auto obj : state.getOwnedObjects()) incomeHandicapped += obj->asOwnable()->dailyIncome(); + if (!state.isHuman()) + { + // Initialize bonuses for different resources + int difficultyIndex = gameHandler->gameState()->getStartInfo()->difficulty; + const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyIndex]; + const JsonNode & weeklyBonusesConfig = gameHandler->gameState()->getSettings().getValue(EGameSettings::RESOURCES_WEEKLY_BONUSES_AI); + const JsonNode & difficultyConfig = weeklyBonusesConfig[difficultyName]; + + // Distribute weekly bonuses over 7 days, depending on the current day of the week + for (GameResID i : GameResID::ALL_RESOURCES()) + { + const std::string & name = GameConstants::RESOURCE_NAMES[i]; + int weeklyBonus = difficultyConfig[name].Integer(); + int dayOfWeek = gameHandler->gameState()->getDate(Date::DAY_OF_WEEK); + int dailyIncome = incomeHandicapped[i]; + int amountTillToday = dailyIncome * weeklyBonus * (dayOfWeek-1) / 7 / 100; + int amountAfterToday = dailyIncome * weeklyBonus * dayOfWeek / 7 / 100; + int dailyBonusToday = amountAfterToday - amountTillToday; + incomeHandicapped[static_cast(i)] += dailyBonusToday; + } + } + return incomeHandicapped; } diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 38b4409d0..89b034128 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -70,7 +70,7 @@ void PlayerMessageProcessor::commandExit(PlayerColor player, const std::vectorgameLobby()->setState(EServerState::SHUTDOWN); } @@ -115,7 +115,11 @@ void PlayerMessageProcessor::commandSave(PlayerColor player, const std::vectorsave("Saves/" + words[1]); - broadcastSystemMessage("game saved as " + words[1]); + MetaString str; + str.appendTextID("vcmi.broadcast.gameSavedAs"); + str.appendRawString(" "); + str.appendRawString(words[1]); + broadcastSystemMessage(str); } } @@ -126,13 +130,15 @@ void PlayerMessageProcessor::commandCheaters(PlayerColor player, const std::vect { if(player.second.cheated) { - broadcastSystemMessage("Player " + player.first.toString() + " is cheater!"); + auto str = MetaString::createFromTextID("vcmi.broadcast.playerCheater"); + str.replaceName(player.first); + broadcastSystemMessage(str); playersCheated++; } } if(!playersCheated) - broadcastSystemMessage("No cheaters registered!"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.noCheater")); } void PlayerMessageProcessor::commandStatistic(PlayerColor player, const std::vector & words) @@ -143,49 +149,51 @@ void PlayerMessageProcessor::commandStatistic(PlayerColor player, const std::vec std::string path = gameHandler->gameState()->statistic.writeCsv(); - broadcastSystemMessage("Statistic files can be found in " + path + " directory\n"); + auto str = MetaString::createFromTextID("vcmi.broadcast.statisticFile"); + str.replaceRawString(path); + broadcastSystemMessage(str); } void PlayerMessageProcessor::commandHelp(PlayerColor player, const std::vector & words) { - broadcastSystemMessage("Available commands to host:"); - broadcastSystemMessage("'!exit' - immediately ends current game"); - broadcastSystemMessage("'!kick ' - kick specified player from the game"); - broadcastSystemMessage("'!save ' - save game under specified filename"); - broadcastSystemMessage("'!statistic' - save game statistics as csv file"); - broadcastSystemMessage("Available commands to all players:"); - broadcastSystemMessage("'!help' - display this help"); - broadcastSystemMessage("'!cheaters' - list players that entered cheat command during game"); - broadcastSystemMessage("'!vote' - allows to change some game settings if all players vote for it"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.commands")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.exit")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.kick")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.save")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.statistic")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.commandsAll")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.help")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.cheaters")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.vote")); } void PlayerMessageProcessor::commandVote(PlayerColor player, const std::vector & words) { if(words.size() < 2) { - broadcastSystemMessage("'!vote simturns allow X' - allow simultaneous turns for specified number of days, or until contact"); - broadcastSystemMessage("'!vote simturns force X' - force simultaneous turns for specified number of days, blocking player contacts"); - broadcastSystemMessage("'!vote simturns abort' - abort simultaneous turns once this turn ends"); - broadcastSystemMessage("'!vote timer prolong X' - prolong base timer for all players by specified number of seconds"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.allow")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.force")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.abort")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.timer")); return; } - if(words[1] == "yes" || words[1] == "no") + if(words[1] == "yes" || words[1] == "no" || words[1] == MetaString::createFromTextID("vcmi.broadcast.vote.yes").toString() || words[1] == MetaString::createFromTextID("vcmi.broadcast.vote.no").toString()) { if(currentVote == ECurrentChatVote::NONE) { - broadcastSystemMessage("No active voting!"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.noActive")); return; } - if(words[1] == "yes") + if(words[1] == "yes" || words[1] == MetaString::createFromTextID("vcmi.broadcast.vote.yes").toString()) { awaitingPlayers.erase(player); if(awaitingPlayers.empty()) finishVoting(); return; } - if(words[1] == "no") + if(words[1] == "no" || words[1] == MetaString::createFromTextID("vcmi.broadcast.vote.no").toString()) { abortVoting(); return; @@ -240,28 +248,36 @@ void PlayerMessageProcessor::commandVote(PlayerColor player, const std::vectorturnOrder->setMaxSimturnsDuration(currentVoteParameter); break; case ECurrentChatVote::SIMTURNS_FORCE: - broadcastSystemMessage("Voting successful. Simultaneous turns will run for " + std::to_string(currentVoteParameter) + " more days. Contacts are blocked"); + msg.appendTextID("vcmi.broadcast.vote.success.contactsBlocked"); + msg.replaceRawString(std::to_string(currentVoteParameter)); + broadcastSystemMessage(msg); gameHandler->turnOrder->setMinSimturnsDuration(currentVoteParameter); break; case ECurrentChatVote::SIMTURNS_ABORT: - broadcastSystemMessage("Voting successful. Simultaneous turns will end on next day"); + msg.appendTextID("vcmi.broadcast.vote.success.nextDay"); + broadcastSystemMessage(msg); gameHandler->turnOrder->setMinSimturnsDuration(0); gameHandler->turnOrder->setMaxSimturnsDuration(0); break; case ECurrentChatVote::TIMER_PROLONG: - broadcastSystemMessage("Voting successful. Timer for all players has been prolonger for " + std::to_string(currentVoteParameter) + " seconds"); + msg.appendTextID("vcmi.broadcast.vote.success.timer"); + msg.replaceRawString(std::to_string(currentVoteParameter)); + broadcastSystemMessage(msg); gameHandler->turnTimerHandler->prolongTimers(currentVoteParameter * 1000); break; } @@ -272,7 +288,7 @@ void PlayerMessageProcessor::finishVoting() void PlayerMessageProcessor::abortVoting() { - broadcastSystemMessage("Player voted against change. Voting aborted"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.aborted")); currentVote = ECurrentChatVote::NONE; } @@ -281,25 +297,33 @@ void PlayerMessageProcessor::startVoting(PlayerColor initiator, ECurrentChatVote currentVote = what; currentVoteParameter = parameter; + MetaString msg; switch(currentVote) { case ECurrentChatVote::SIMTURNS_ALLOW: - broadcastSystemMessage("Started voting to allow simultaneous turns for " + std::to_string(parameter) + " more days"); + msg.appendTextID("vcmi.broadcast.vote.start.untilContacts"); + msg.replaceRawString(std::to_string(parameter)); + broadcastSystemMessage(msg); break; case ECurrentChatVote::SIMTURNS_FORCE: - broadcastSystemMessage("Started voting to force simultaneous turns for " + std::to_string(parameter) + " more days"); + msg.appendTextID("vcmi.broadcast.vote.start.contactsBlocked"); + msg.replaceRawString(std::to_string(parameter)); + broadcastSystemMessage(msg); break; case ECurrentChatVote::SIMTURNS_ABORT: - broadcastSystemMessage("Started voting to end simultaneous turns starting from next day"); + msg.appendTextID("vcmi.broadcast.vote.start.nextDay"); + broadcastSystemMessage(msg); break; case ECurrentChatVote::TIMER_PROLONG: - broadcastSystemMessage("Started voting to prolong timer for all players by " + std::to_string(parameter) + " seconds"); + msg.appendTextID("vcmi.broadcast.vote.start.timer"); + msg.replaceRawString(std::to_string(parameter)); + broadcastSystemMessage(msg); break; default: return; } - broadcastSystemMessage("Type '!vote yes' to agree to this change or '!vote no' to vote against it"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.hint")); awaitingPlayers.clear(); for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 0f02ccebf..899df2b04 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -72,7 +72,7 @@ void TurnOrderProcessor::updateAndNotifyContactStatus() { // Simturns between all players have ended - send single global notification if (!blockedContacts.empty()) - gameHandler->playerMessages->broadcastSystemMessage("Simultaneous turns have ended"); + gameHandler->playerMessages->broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.simturn.end")); } else { @@ -83,11 +83,11 @@ void TurnOrderProcessor::updateAndNotifyContactStatus() continue; MetaString message; - message.appendRawString("Simultaneous turns between players %s and %s have ended"); // FIXME: we should send MetaString itself and localize it on client side + message.appendTextID("vcmi.broadcast.simturn.endBetween"); message.replaceName(contact.a); message.replaceName(contact.b); - gameHandler->playerMessages->broadcastSystemMessage(message.toString()); + gameHandler->playerMessages->broadcastSystemMessage(message); } } diff --git a/vcmiqt/jsonutils.cpp b/vcmiqt/jsonutils.cpp index e4eb884d6..5815a46be 100644 --- a/vcmiqt/jsonutils.cpp +++ b/vcmiqt/jsonutils.cpp @@ -79,7 +79,7 @@ QVariant toVariant(const JsonNode & node) return QVariant(); } -QVariant JsonFromFile(QString filename) +JsonNode jsonFromFile(QString filename) { QFile file(filename); if(!file.open(QFile::ReadOnly)) @@ -90,7 +90,7 @@ QVariant JsonFromFile(QString filename) const auto data = file.readAll(); JsonNode node(reinterpret_cast(data.data()), data.size(), filename.toStdString()); - return toVariant(node); + return node; } JsonNode toJson(QVariant object) @@ -113,10 +113,10 @@ JsonNode toJson(QVariant object) return ret; } -void JsonToFile(QString filename, QVariant object) +void jsonToFile(QString filename, const JsonNode & object) { std::fstream file(qstringToPath(filename).c_str(), std::ios::out | std::ios_base::binary); - file << toJson(object).toString(); + file << object.toCompactString(); } } diff --git a/vcmiqt/jsonutils.h b/vcmiqt/jsonutils.h index 1b78881d6..f65d666b4 100644 --- a/vcmiqt/jsonutils.h +++ b/vcmiqt/jsonutils.h @@ -20,10 +20,10 @@ class JsonNode; namespace JsonUtils { VCMIQT_LINKAGE QVariant toVariant(const JsonNode & node); -VCMIQT_LINKAGE QVariant JsonFromFile(QString filename); +VCMIQT_LINKAGE JsonNode jsonFromFile(QString filename); VCMIQT_LINKAGE JsonNode toJson(QVariant object); -VCMIQT_LINKAGE void JsonToFile(QString filename, QVariant object); +VCMIQT_LINKAGE void jsonToFile(QString filename, const JsonNode & object); } VCMI_LIB_NAMESPACE_END