mirror of
https://github.com/vcmi/vcmi.git
synced 2025-04-23 12:08:45 +02:00
commit
370ecf6f66
@ -167,14 +167,12 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
|
||||
|
||||
result = evaluator.selectStackAction(stack);
|
||||
|
||||
if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell())
|
||||
if(autobattlePreferences.enableSpellsUsage && evaluator.canCastSpell())
|
||||
{
|
||||
auto spelCasted = evaluator.attemptCastingSpell(stack);
|
||||
|
||||
if(spelCasted)
|
||||
return;
|
||||
|
||||
skipCastUntilNextBattle = true;
|
||||
}
|
||||
|
||||
logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
|
||||
@ -256,8 +254,6 @@ void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1
|
||||
{
|
||||
LOG_TRACE(logAi);
|
||||
side = Side;
|
||||
|
||||
skipCastUntilNextBattle = false;
|
||||
}
|
||||
|
||||
void CBattleAI::print(const std::string &text) const
|
||||
|
@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface
|
||||
bool wasWaitingForRealize;
|
||||
bool wasUnlockingGs;
|
||||
int movesSkippedByDefense;
|
||||
bool skipCastUntilNextBattle;
|
||||
|
||||
public:
|
||||
CBattleAI();
|
||||
|
@ -119,6 +119,14 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
|
||||
return result;
|
||||
}
|
||||
|
||||
bool BattleEvaluator::hasWorkingTowers() const
|
||||
{
|
||||
bool keepIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
|
||||
bool upperIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
|
||||
bool bottomIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
|
||||
return keepIntact || upperIntact || bottomIntact;
|
||||
}
|
||||
|
||||
std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
|
||||
{
|
||||
//TODO: faerie dragon type spell should be selected by server
|
||||
@ -161,6 +169,14 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
|
||||
|
||||
auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
|
||||
float score = EvaluationResult::INEFFECTIVE_SCORE;
|
||||
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
|
||||
{
|
||||
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
|
||||
});
|
||||
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
|
||||
&& !stack->canShoot()
|
||||
&& hasWorkingTowers()
|
||||
&& !enemyMellee.empty();
|
||||
|
||||
if(targets->possibleAttacks.empty() && bestSpellcast.has_value())
|
||||
{
|
||||
@ -174,7 +190,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
|
||||
logAi->trace("Evaluating attack for %s", stack->getDescription());
|
||||
#endif
|
||||
|
||||
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
|
||||
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense);
|
||||
auto & bestAttack = evaluationResult.bestAttack;
|
||||
|
||||
cachedAttack.ap = bestAttack;
|
||||
@ -227,15 +243,10 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
|
||||
return BattleAction::makeDefend(stack);
|
||||
}
|
||||
|
||||
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool
|
||||
{
|
||||
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
|
||||
});
|
||||
|
||||
bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4;
|
||||
bool isTargetOutsideFort = !hb->battleIsInsideWalls(bestAttack.from);
|
||||
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
|
||||
&& !bestAttack.attack.shooting
|
||||
&& hb->battleGetFortifications().hasMoat
|
||||
&& hasWorkingTowers()
|
||||
&& !enemyMellee.empty()
|
||||
&& isTargetOutsideFort;
|
||||
|
||||
@ -349,6 +360,22 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
|
||||
auto reachability = cb->getBattle(battleID)->getReachability(stack);
|
||||
auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
|
||||
|
||||
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
|
||||
{
|
||||
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
|
||||
});
|
||||
|
||||
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
|
||||
&& hasWorkingTowers()
|
||||
&& !enemyMellee.empty();
|
||||
|
||||
if (siegeDefense)
|
||||
{
|
||||
vstd::erase_if(avHexes, [&](const BattleHex& hex) {
|
||||
return !cb->getBattle(battleID)->battleIsInsideWalls(hex);
|
||||
});
|
||||
}
|
||||
|
||||
if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
|
||||
{
|
||||
return BattleAction::makeDefend(stack);
|
||||
|
@ -53,6 +53,7 @@ public:
|
||||
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
|
||||
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
|
||||
std::vector<BattleHex> getBrokenWallMoatHexes() const;
|
||||
bool hasWorkingTowers() const;
|
||||
void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
|
||||
void print(const std::string & text) const;
|
||||
BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);
|
||||
|
@ -9,6 +9,7 @@
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "BattleExchangeVariant.h"
|
||||
#include "BattleEvaluator.h"
|
||||
#include "../../lib/CStack.h"
|
||||
|
||||
AttackerValue::AttackerValue()
|
||||
@ -213,7 +214,8 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
|
||||
const battle::Unit * activeStack,
|
||||
PotentialTargets & targets,
|
||||
DamageCache & damageCache,
|
||||
std::shared_ptr<HypotheticBattle> hb)
|
||||
std::shared_ptr<HypotheticBattle> hb,
|
||||
bool siegeDefense)
|
||||
{
|
||||
EvaluationResult result(targets.bestAction());
|
||||
|
||||
@ -231,6 +233,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
|
||||
|
||||
for(auto & ap : targets.possibleAttacks)
|
||||
{
|
||||
if (siegeDefense && !hb->battleIsInsideWalls(ap.from))
|
||||
continue;
|
||||
|
||||
float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
|
||||
|
||||
if(score > result.score)
|
||||
@ -263,6 +268,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
|
||||
|
||||
for(auto & ap : targets.possibleAttacks)
|
||||
{
|
||||
if (siegeDefense && !hb->battleIsInsideWalls(ap.from))
|
||||
continue;
|
||||
|
||||
float score = evaluateExchange(ap, 0, targets, damageCache, hb);
|
||||
bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
|
||||
|
||||
@ -350,11 +358,32 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
|
||||
if(distance <= speed)
|
||||
continue;
|
||||
|
||||
float penaltyMultiplier = 1.0f; // Default multiplier, no penalty
|
||||
float closestAllyDistance = std::numeric_limits<float>::max();
|
||||
|
||||
for (const battle::Unit* ally : hb->battleAliveUnits()) {
|
||||
if (ally == activeStack)
|
||||
continue;
|
||||
if (ally->unitSide() != activeStack->unitSide())
|
||||
continue;
|
||||
|
||||
float allyDistance = dists.distToNearestNeighbour(ally, enemy);
|
||||
if (allyDistance < closestAllyDistance)
|
||||
{
|
||||
closestAllyDistance = allyDistance;
|
||||
}
|
||||
}
|
||||
|
||||
// If an ally is closer to the enemy, compute the penaltyMultiplier
|
||||
if (closestAllyDistance < distance) {
|
||||
penaltyMultiplier = closestAllyDistance / distance; // Ratio of distances
|
||||
}
|
||||
|
||||
auto turnsToRich = (distance - 1) / speed + 1;
|
||||
auto hexes = enemy->getSurroundingHexes();
|
||||
auto enemySpeed = enemy->getMovementRange();
|
||||
auto speedRatio = speed / static_cast<float>(enemySpeed);
|
||||
auto multiplier = speedRatio > 1 ? 1 : speedRatio;
|
||||
auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
|
||||
|
||||
for(auto & hex : hexes)
|
||||
{
|
||||
|
@ -159,7 +159,8 @@ public:
|
||||
const battle::Unit * activeStack,
|
||||
PotentialTargets & targets,
|
||||
DamageCache & damageCache,
|
||||
std::shared_ptr<HypotheticBattle> hb);
|
||||
std::shared_ptr<HypotheticBattle> hb,
|
||||
bool siegeDefense = false);
|
||||
|
||||
float evaluateExchange(
|
||||
const AttackPossibility & ap,
|
||||
|
@ -309,6 +309,8 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
|
||||
? dynamic_cast<const CGTownInstance *>(dwelling)
|
||||
: nullptr;
|
||||
|
||||
std::set<SlotID> alreadyDisbanded;
|
||||
|
||||
for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
|
||||
{
|
||||
auto ci = infoFromDC(dwelling->creatures[i]);
|
||||
@ -322,18 +324,71 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
|
||||
|
||||
if(!ci.count) continue;
|
||||
|
||||
// Calculate the market value of the new stack
|
||||
TResources newStackValue = ci.creID.toCreature()->getFullRecruitCost() * ci.count;
|
||||
|
||||
SlotID dst = hero->getSlotFor(ci.creID);
|
||||
|
||||
// Keep track of the least valuable slot in the hero's army
|
||||
SlotID leastValuableSlot;
|
||||
TResources leastValuableStackValue;
|
||||
leastValuableStackValue[6] = std::numeric_limits<int>::max();
|
||||
bool shouldDisband = false;
|
||||
if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
|
||||
{
|
||||
if(!freeHeroSlots) //no more place for stacks
|
||||
if(!freeHeroSlots) // No free slots; consider replacing
|
||||
{
|
||||
// Check for the least valuable existing stack
|
||||
for (auto& slot : hero->Slots())
|
||||
{
|
||||
if (alreadyDisbanded.find(slot.first) != alreadyDisbanded.end())
|
||||
continue;
|
||||
|
||||
if(slot.second->getCreatureID() != CreatureID::NONE)
|
||||
{
|
||||
TResources currentStackValue = slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->getCount();
|
||||
|
||||
if (town && slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
|
||||
continue;
|
||||
|
||||
if(currentStackValue.marketValue() < leastValuableStackValue.marketValue())
|
||||
{
|
||||
leastValuableStackValue = currentStackValue;
|
||||
leastValuableSlot = slot.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decide whether to replace the least valuable stack
|
||||
if(newStackValue.marketValue() <= leastValuableStackValue.marketValue())
|
||||
{
|
||||
continue; // Skip if the new stack isn't worth replacing
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldDisband = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
freeHeroSlots--; //new slot will be occupied
|
||||
}
|
||||
}
|
||||
|
||||
vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
|
||||
|
||||
if(!ci.count) continue;
|
||||
int disbandMalus = 0;
|
||||
|
||||
if (shouldDisband)
|
||||
{
|
||||
disbandMalus = leastValuableStackValue / ci.creID.toCreature()->getFullRecruitCost();
|
||||
alreadyDisbanded.insert(leastValuableSlot);
|
||||
}
|
||||
|
||||
ci.count -= disbandMalus;
|
||||
|
||||
if(ci.count <= 0)
|
||||
continue;
|
||||
|
||||
ci.level = i; //this is important for Dungeon Summoning Portal
|
||||
creaturesInDwellings.push_back(ci);
|
||||
|
@ -505,7 +505,7 @@ void ObjectClusterizer::clusterizeObject(
|
||||
else if (priority <= 0)
|
||||
continue;
|
||||
|
||||
bool interestingObject = path.turn() <= 2 || priority > 0.5f;
|
||||
bool interestingObject = path.turn() <= 2 || priority > (ai->settings->isUseFuzzy() ? 0.5f : 0);
|
||||
|
||||
if(interestingObject)
|
||||
{
|
||||
|
@ -64,7 +64,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
|
||||
|
||||
if(reinforcement)
|
||||
{
|
||||
tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(5)));
|
||||
tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(reinforcement)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,9 +41,6 @@ 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;
|
||||
@ -422,6 +419,21 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
|
||||
if(hero->getTotalStrength() < threat.danger)
|
||||
continue;
|
||||
|
||||
bool heroAlreadyHiredInOtherTown = false;
|
||||
for (const auto& task : tasks)
|
||||
{
|
||||
if (auto recruitGoal = dynamic_cast<Goals::RecruitHero*>(task.get()))
|
||||
{
|
||||
if (recruitGoal->getHero() == hero)
|
||||
{
|
||||
heroAlreadyHiredInOtherTown = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (heroAlreadyHiredInOtherTown)
|
||||
continue;
|
||||
|
||||
auto myHeroes = ai->cb->getHeroesInfo();
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
|
@ -124,6 +124,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
|
||||
{
|
||||
if (ai->cb->getHeroesInfo().size() == 0
|
||||
|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
|
||||
|| bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0
|
||||
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
|
||||
|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
|
||||
{
|
||||
|
@ -397,7 +397,12 @@ void Nullkiller::makeTurn()
|
||||
if(!executeTask(bestTask))
|
||||
return;
|
||||
|
||||
updateAiState(i, true);
|
||||
bool fastUpdate = true;
|
||||
|
||||
if (bestTask->getHero() != nullptr)
|
||||
fastUpdate = false;
|
||||
|
||||
updateAiState(i, fastUpdate);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1006,6 +1006,9 @@ public:
|
||||
Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task);
|
||||
const AIPath & path = chain.getPath();
|
||||
|
||||
if (vstd::isAlmostZero(path.movementCost()))
|
||||
return;
|
||||
|
||||
vstd::amax(evaluationContext.danger, path.getTotalDanger());
|
||||
evaluationContext.movementCost += path.movementCost();
|
||||
evaluationContext.closestWayRatio = chain.closestWayRatio;
|
||||
@ -1019,12 +1022,20 @@ public:
|
||||
evaluationContext.involvesSailing = true;
|
||||
}
|
||||
|
||||
float highestCostForSingleHero = 0;
|
||||
for(auto pair : costsPerHero)
|
||||
{
|
||||
auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first);
|
||||
|
||||
evaluationContext.movementCostByRole[role] += pair.second;
|
||||
if (pair.second > highestCostForSingleHero)
|
||||
highestCostForSingleHero = pair.second;
|
||||
}
|
||||
if (highestCostForSingleHero > 1 && costsPerHero.size() > 1)
|
||||
{
|
||||
//Chains that involve more than 1 hero doing something for more than a turn are too expensive in my book. They often involved heroes doing nothing just standing there waiting to fulfill their part of the chain.
|
||||
return;
|
||||
}
|
||||
evaluationContext.movementCost *= costsPerHero.size(); //further deincentivise chaining as it often involves bringing back the army afterwards
|
||||
|
||||
auto hero = task->hero;
|
||||
bool checkGold = evaluationContext.danger == 0;
|
||||
@ -1046,13 +1057,13 @@ public:
|
||||
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)
|
||||
if (target->getOwner().isValidPlayer() && 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();
|
||||
}
|
||||
evaluationContext.armyInvolvement += army->getArmyCost();
|
||||
|
||||
vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
|
||||
addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
|
||||
@ -1353,17 +1364,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
|
||||
|
||||
bool arriveNextWeek = false;
|
||||
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
|
||||
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL)
|
||||
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",
|
||||
logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %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 isEnemy: %d arriveNextWeek: %d",
|
||||
priorityTier,
|
||||
task->toString(),
|
||||
evaluationContext.armyLossPersentage,
|
||||
(int)evaluationContext.turn,
|
||||
evaluationContext.movementCostByRole[HeroRole::MAIN],
|
||||
evaluationContext.movementCostByRole[HeroRole::SCOUT],
|
||||
evaluationContext.armyInvolvement,
|
||||
goldRewardPerTurn,
|
||||
evaluationContext.goldCost,
|
||||
evaluationContext.armyReward,
|
||||
@ -1378,7 +1390,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
evaluationContext.closestWayRatio,
|
||||
evaluationContext.enemyHeroDangerRatio,
|
||||
evaluationContext.explorePriority,
|
||||
evaluationContext.isDefend);
|
||||
evaluationContext.isDefend,
|
||||
evaluationContext.isEnemy,
|
||||
arriveNextWeek);
|
||||
#endif
|
||||
|
||||
switch (priorityTier)
|
||||
@ -1387,13 +1401,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
{
|
||||
if (evaluationContext.turn > 0)
|
||||
return 0;
|
||||
if (evaluationContext.movementCost >= 1)
|
||||
return 0;
|
||||
if(evaluationContext.conquestValue > 0)
|
||||
score = 1000;
|
||||
score = evaluationContext.armyInvolvement;
|
||||
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;
|
||||
@ -1404,17 +1419,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
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
|
||||
//FALL_THROUGH
|
||||
case PriorityTier::FAR_KILL:
|
||||
{
|
||||
if (evaluationContext.turn > 0 && evaluationContext.isHero)
|
||||
return 0;
|
||||
if (arriveNextWeek && evaluationContext.isEnemy)
|
||||
return 0;
|
||||
if (evaluationContext.conquestValue > 0)
|
||||
score = 1000;
|
||||
score = evaluationContext.armyInvolvement;
|
||||
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
@ -1432,8 +1448,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
|
||||
return 0;
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
if (evaluationContext.movementCost > 0)
|
||||
score /= evaluationContext.movementCost;
|
||||
break;
|
||||
@ -1446,13 +1463,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
|
||||
return 0;
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
if (evaluationContext.movementCost > 0)
|
||||
score /= evaluationContext.movementCost;
|
||||
break;
|
||||
}
|
||||
case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
|
||||
//FALL_THROUGH
|
||||
case PriorityTier::FAR_HUNTER_GATHER:
|
||||
{
|
||||
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
|
||||
return 0;
|
||||
@ -1468,6 +1488,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
|
||||
return 0;
|
||||
score += evaluationContext.strategicalValue * 1000;
|
||||
score += evaluationContext.goldReward;
|
||||
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
|
||||
@ -1478,7 +1500,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
if (score > 0)
|
||||
{
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
if (evaluationContext.movementCost > 0)
|
||||
score /= evaluationContext.movementCost;
|
||||
}
|
||||
@ -1492,8 +1513,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
if (evaluationContext.closestWayRatio < 1.0)
|
||||
return 0;
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
if (evaluationContext.movementCost > 0)
|
||||
score /= evaluationContext.movementCost;
|
||||
break;
|
||||
@ -1503,8 +1525,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
|
||||
return 0;
|
||||
if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
score = evaluationContext.armyInvolvement;
|
||||
score /= (evaluationContext.turn + 1);
|
||||
break;
|
||||
}
|
||||
@ -1563,13 +1584,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
}
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
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",
|
||||
logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %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,
|
||||
evaluationContext.movementCostByRole[HeroRole::MAIN],
|
||||
evaluationContext.movementCostByRole[HeroRole::SCOUT],
|
||||
evaluationContext.armyInvolvement,
|
||||
goldRewardPerTurn,
|
||||
evaluationContext.goldCost,
|
||||
evaluationContext.armyReward,
|
||||
|
@ -118,6 +118,8 @@ public:
|
||||
HIGH_PRIO_EXPLORE,
|
||||
HUNTER_GATHER,
|
||||
LOW_PRIO_EXPLORE,
|
||||
FAR_KILL,
|
||||
FAR_HUNTER_GATHER,
|
||||
DEFEND
|
||||
};
|
||||
|
||||
|
@ -57,8 +57,37 @@ void BuyArmy::accept(AIGateway * ai)
|
||||
vstd::amin(ci.count, res / ci.creID.toCreature()->getFullRecruitCost());
|
||||
|
||||
if(ci.count)
|
||||
{
|
||||
if (town->getUpperArmy()->stacksCount() == GameConstants::ARMY_SIZE)
|
||||
{
|
||||
SlotID lowestValueSlot;
|
||||
int lowestValue = std::numeric_limits<int>::max();
|
||||
for (auto slot : town->getUpperArmy()->Slots())
|
||||
{
|
||||
if (slot.second->getCreatureID() != CreatureID::NONE)
|
||||
{
|
||||
int currentStackMarketValue =
|
||||
slot.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * slot.second->getCount();
|
||||
|
||||
if (slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
|
||||
continue;
|
||||
|
||||
if (currentStackMarketValue < lowestValue)
|
||||
{
|
||||
lowestValue = currentStackMarketValue;
|
||||
lowestValueSlot = slot.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lowestValueSlot.validSlot())
|
||||
{
|
||||
cb->dismissCreature(town->getUpperArmy(), lowestValueSlot);
|
||||
}
|
||||
}
|
||||
if (town->getUpperArmy()->stacksCount() < GameConstants::ARMY_SIZE || town->getUpperArmy()->getSlotFor(ci.creID).validSlot()) //It is possible we don't scrap despite we wanted to due to not scrapping stacks that fit our faction
|
||||
{
|
||||
cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
|
||||
}
|
||||
valueBought += ci.count * ci.creID.toCreature()->getAIValue();
|
||||
}
|
||||
}
|
||||
|
@ -374,12 +374,14 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
|
||||
for(auto & creatureToBuy : buyArmy)
|
||||
{
|
||||
auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature());
|
||||
|
||||
if (targetSlot.validSlot())
|
||||
{
|
||||
target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
|
||||
target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count;
|
||||
target->requireBuyArmy = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(target->getArmyStrength() <= army->getArmyStrength())
|
||||
{
|
||||
|
@ -567,56 +567,62 @@ FunctionEnd
|
||||
;Languages
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" ;first language is the default language
|
||||
!insertmacro MUI_LANGUAGE "Albanian"
|
||||
!insertmacro MUI_LANGUAGE "Arabic"
|
||||
!insertmacro MUI_LANGUAGE "Basque"
|
||||
!insertmacro MUI_LANGUAGE "Belarusian"
|
||||
!insertmacro MUI_LANGUAGE "Bosnian"
|
||||
!insertmacro MUI_LANGUAGE "Breton"
|
||||
!insertmacro MUI_LANGUAGE "Bulgarian"
|
||||
!insertmacro MUI_LANGUAGE "Croatian"
|
||||
!insertmacro MUI_LANGUAGE "Czech"
|
||||
!insertmacro MUI_LANGUAGE "Danish"
|
||||
!insertmacro MUI_LANGUAGE "Dutch"
|
||||
!insertmacro MUI_LANGUAGE "Estonian"
|
||||
!insertmacro MUI_LANGUAGE "Farsi"
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
!insertmacro MUI_LANGUAGE "Finnish"
|
||||
!insertmacro MUI_LANGUAGE "French"
|
||||
!insertmacro MUI_LANGUAGE "German"
|
||||
!insertmacro MUI_LANGUAGE "Greek"
|
||||
!insertmacro MUI_LANGUAGE "Hebrew"
|
||||
!insertmacro MUI_LANGUAGE "Hungarian"
|
||||
!insertmacro MUI_LANGUAGE "Icelandic"
|
||||
!insertmacro MUI_LANGUAGE "Indonesian"
|
||||
!insertmacro MUI_LANGUAGE "Irish"
|
||||
!insertmacro MUI_LANGUAGE "Italian"
|
||||
!insertmacro MUI_LANGUAGE "Japanese"
|
||||
!insertmacro MUI_LANGUAGE "Korean"
|
||||
!insertmacro MUI_LANGUAGE "Kurdish"
|
||||
!insertmacro MUI_LANGUAGE "Latvian"
|
||||
!insertmacro MUI_LANGUAGE "Lithuanian"
|
||||
!insertmacro MUI_LANGUAGE "Luxembourgish"
|
||||
!insertmacro MUI_LANGUAGE "Macedonian"
|
||||
!insertmacro MUI_LANGUAGE "Malay"
|
||||
!insertmacro MUI_LANGUAGE "Mongolian"
|
||||
!insertmacro MUI_LANGUAGE "Norwegian"
|
||||
!insertmacro MUI_LANGUAGE "Polish"
|
||||
!insertmacro MUI_LANGUAGE "Portuguese"
|
||||
!insertmacro MUI_LANGUAGE "PortugueseBR"
|
||||
!insertmacro MUI_LANGUAGE "Romanian"
|
||||
!insertmacro MUI_LANGUAGE "Russian"
|
||||
!insertmacro MUI_LANGUAGE "Serbian"
|
||||
!insertmacro MUI_LANGUAGE "SerbianLatin"
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
!insertmacro MUI_LANGUAGE "Slovak"
|
||||
!insertmacro MUI_LANGUAGE "Slovenian"
|
||||
!insertmacro MUI_LANGUAGE "Spanish"
|
||||
!insertmacro MUI_LANGUAGE "Swedish"
|
||||
!insertmacro MUI_LANGUAGE "Thai"
|
||||
!insertmacro MUI_LANGUAGE "TradChinese"
|
||||
!insertmacro MUI_LANGUAGE "Turkish"
|
||||
!insertmacro MUI_LANGUAGE "Ukrainian"
|
||||
!insertmacro MUI_LANGUAGE "Welsh"
|
||||
!insertmacro MUI_LANGUAGE "Vietnamese"
|
||||
|
||||
;!insertmacro MUI_LANGUAGE "Albanian"
|
||||
;!insertmacro MUI_LANGUAGE "Arabic"
|
||||
;!insertmacro MUI_LANGUAGE "Basque"
|
||||
;!insertmacro MUI_LANGUAGE "Belarusian"
|
||||
;!insertmacro MUI_LANGUAGE "Bosnian"
|
||||
;!insertmacro MUI_LANGUAGE "Breton"
|
||||
;!insertmacro MUI_LANGUAGE "Bulgarian"
|
||||
;!insertmacro MUI_LANGUAGE "Croatian"
|
||||
;!insertmacro MUI_LANGUAGE "Danish"
|
||||
;!insertmacro MUI_LANGUAGE "Dutch"
|
||||
;!insertmacro MUI_LANGUAGE "Estonian"
|
||||
;!insertmacro MUI_LANGUAGE "Farsi"
|
||||
;!insertmacro MUI_LANGUAGE "Greek"
|
||||
;!insertmacro MUI_LANGUAGE "Hebrew"
|
||||
;!insertmacro MUI_LANGUAGE "Icelandic"
|
||||
;!insertmacro MUI_LANGUAGE "Indonesian"
|
||||
;!insertmacro MUI_LANGUAGE "Irish"
|
||||
;!insertmacro MUI_LANGUAGE "Japanese"
|
||||
;!insertmacro MUI_LANGUAGE "Kurdish"
|
||||
;!insertmacro MUI_LANGUAGE "Latvian"
|
||||
;!insertmacro MUI_LANGUAGE "Lithuanian"
|
||||
;!insertmacro MUI_LANGUAGE "Luxembourgish"
|
||||
;!insertmacro MUI_LANGUAGE "Macedonian"
|
||||
;!insertmacro MUI_LANGUAGE "Malay"
|
||||
;!insertmacro MUI_LANGUAGE "Mongolian"
|
||||
;!insertmacro MUI_LANGUAGE "Norwegian"
|
||||
;!insertmacro MUI_LANGUAGE "PortugueseBR"
|
||||
;!insertmacro MUI_LANGUAGE "Romanian"
|
||||
;!insertmacro MUI_LANGUAGE "Serbian"
|
||||
;!insertmacro MUI_LANGUAGE "SerbianLatin"
|
||||
;!insertmacro MUI_LANGUAGE "Slovak"
|
||||
;!insertmacro MUI_LANGUAGE "Slovenian"
|
||||
;!insertmacro MUI_LANGUAGE "Thai"
|
||||
;!insertmacro MUI_LANGUAGE "TradChinese"
|
||||
;!insertmacro MUI_LANGUAGE "Welsh"
|
||||
|
||||
|
||||
; Language Selection Dialog
|
||||
!define MUI_LANGDLL_DISPLAY
|
||||
|
||||
|
||||
;--------------------------------
|
||||
@ -899,6 +905,9 @@ SectionEnd
|
||||
; "Program Files" for AllUsers, "My Documents" for JustMe...
|
||||
|
||||
Function .onInit
|
||||
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
|
||||
StrCmp "@CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL@" "ON" 0 inst
|
||||
|
||||
ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "UninstallString"
|
||||
|
13
ChangeLog.md
13
ChangeLog.md
@ -1,10 +1,11 @@
|
||||
# VCMI Project Changelog
|
||||
|
||||
## 1.5.7 -> 1.6.0 (in development)
|
||||
## 1.5.7 -> 1.6.0
|
||||
|
||||
### Major changes
|
||||
|
||||
* Greatly improved decision-making of NullkillerAI
|
||||
* Implemented support for multiple mod presets allowing player to quickly switch between them in Launcher
|
||||
* 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
|
||||
@ -66,6 +67,7 @@
|
||||
* 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
|
||||
* The Transport Artifact victory condition fulfilled by the enemy AI will no longer trigger a victory for human players if "standard victory" is enabled on the map
|
||||
|
||||
### Video / Audio
|
||||
|
||||
@ -178,6 +180,12 @@
|
||||
|
||||
### Launcher
|
||||
|
||||
* Implemented support for multiple mod presets allowing player to quickly switch between them
|
||||
* Added new Start Game page to Launcher which is now used when starting the game
|
||||
* Added option to create empty mod preset to quickly disable all mods
|
||||
* Added button to update all installed mods to Start Game page
|
||||
* Added diagnostics to detect common issues with Heroes III data files
|
||||
* Added built-in help descriptions for functionalities such as data files import to better explain them to players
|
||||
* 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
|
||||
@ -188,6 +196,8 @@
|
||||
* 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
|
||||
* It is no longer possible to start installation or update for a mod that is already being downloaded
|
||||
* Fixed detection of existing Heroes III Complete or Shadow of Death data files during import
|
||||
|
||||
### Map Editor
|
||||
|
||||
@ -205,6 +215,7 @@
|
||||
* Fixed duplicated list of spells in Mage Guild in copy-pasted towns
|
||||
* 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
|
||||
|
@ -121,6 +121,44 @@
|
||||
"vcmi.lobby.deleteFolder" : "你确定要删除下列文件夹?",
|
||||
"vcmi.lobby.deleteMode" : "切换删除模式并返回",
|
||||
|
||||
"vcmi.broadcast.failedLoadGame" : "加载游戏失败",
|
||||
"vcmi.broadcast.command" : "输入'!help'来列举可用命令",
|
||||
"vcmi.broadcast.simturn.end" : "同步回合已结束",
|
||||
"vcmi.broadcast.simturn.endBetween" : "在玩家 %s和%s之间的同步回合已结束",
|
||||
"vcmi.broadcast.serverProblem" : "服务器遇到了一个问题",
|
||||
"vcmi.broadcast.gameTerminated" : "游戏已终止",
|
||||
"vcmi.broadcast.gameSavedAs" : "游戏另存为",
|
||||
"vcmi.broadcast.noCheater" : "没有注册作弊者!",
|
||||
"vcmi.broadcast.playerCheater" : "玩家%s是作弊者!",
|
||||
"vcmi.broadcast.statisticFile" : "统计文件可以在目录%s中找到",
|
||||
"vcmi.broadcast.help.commands" : "主机可用命令:",
|
||||
"vcmi.broadcast.help.exit" : "'!exit' - 立即结束当前游戏",
|
||||
"vcmi.broadcast.help.kick" : "'!kick <玩家>' - 从游戏中踢除特定玩家",
|
||||
"vcmi.broadcast.help.save" : "'!save <文件名>' - 以指定文件名保存游戏",
|
||||
"vcmi.broadcast.help.statistic" : "'!statistic' - 将游戏统计信息保存为csv文件",
|
||||
"vcmi.broadcast.help.commandsAll" : "所有玩家可用命令:",
|
||||
"vcmi.broadcast.help.help" : "'!help' - 显示此帮助",
|
||||
"vcmi.broadcast.help.cheaters" : "'!cheaters' - 列出在游戏中使用作弊命令的玩家",
|
||||
"vcmi.broadcast.help.vote" : "'!vote' - 如果所有玩家投票通过,允许更改一些游戏设置",
|
||||
"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - 允许进行指定天数的同步回合,发生接触解除",
|
||||
"vcmi.broadcast.vote.force" : "'!vote simturns force X' - 强制进行指定天数的同步回合,阻止玩家接触",
|
||||
"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - 本回合结束后中止同步回合",
|
||||
"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - 将所有玩家的基础计时器延长指定的秒数",
|
||||
"vcmi.broadcast.vote.noActive" : "没有正在进行的投票!",
|
||||
"vcmi.broadcast.vote.yes" : "是",
|
||||
"vcmi.broadcast.vote.no" : "否",
|
||||
"vcmi.broadcast.vote.notRecognized" : "投票命令无法识别!",
|
||||
"vcmi.broadcast.vote.success.untilContacts" : "投票成功,同步回合将继续进行%s天,发生接触解除",
|
||||
"vcmi.broadcast.vote.success.contactsBlocked" : "投票成功,同步回合将继续进行%s天,阻止玩家接触",
|
||||
"vcmi.broadcast.vote.success.nextDay" : "投票成功,将于第二天结束同步回合",
|
||||
"vcmi.broadcast.vote.success.timer" : "投票成功,所有玩家的计时器已延长 %s 秒。",
|
||||
"vcmi.broadcast.vote.aborted" : "玩家投票反对更改,投票已中止。",
|
||||
"vcmi.broadcast.vote.start.untilContacts" : "开始投票,允许同步回合再进行 %s 天",
|
||||
"vcmi.broadcast.vote.start.contactsBlocked" : "开始投票, 允许同步回合再强制进行 %s 天",
|
||||
"vcmi.broadcast.vote.start.nextDay" : "开始投票,从第二天起结束同布回合",
|
||||
"vcmi.broadcast.vote.start.timer" : "开始投票,将所有玩家的计时器延长 %s 秒。",
|
||||
"vcmi.broadcast.vote.hint" : "输入'!vote yes'来同意这项改动或输入'!vote no'来投票反对它。",
|
||||
|
||||
"vcmi.lobby.login.title" : "VCMI大厅",
|
||||
"vcmi.lobby.login.username" : "用户名:",
|
||||
"vcmi.lobby.login.connecting" : "连接中...",
|
||||
@ -128,6 +166,7 @@
|
||||
"vcmi.lobby.login.create" : "新账号",
|
||||
"vcmi.lobby.login.login" : "登录",
|
||||
"vcmi.lobby.login.as" : "以 %s 身份登录",
|
||||
"vcmi.lobby.login.spectator" : "旁观者",
|
||||
"vcmi.lobby.header.rooms" : "游戏房间 - %d",
|
||||
"vcmi.lobby.header.channels" : "聊天频道",
|
||||
"vcmi.lobby.header.chat.global" : "全局游戏聊天 - %s", // %s -> language name
|
||||
@ -189,6 +228,8 @@
|
||||
"vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}",
|
||||
"vcmi.server.errors.modsToDisable" : "{需要禁用的mod列表}",
|
||||
"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
|
||||
"vcmi.server.errors.wrongIdentified" : "你被识别为玩家%s,但预期是玩家%s。",
|
||||
"vcmi.server.errors.notAllowed" : "你无权执行此操作!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。",
|
||||
|
||||
|
@ -53,7 +53,7 @@
|
||||
"vcmi.quickExchange.moveAllUnits" : "Přesunout všechny jednotky",
|
||||
"vcmi.quickExchange.swapAllUnits" : "Vyměnit armády",
|
||||
"vcmi.quickExchange.moveAllArtifacts" : "Přesunout všechny artefakty",
|
||||
"vcmi.quickExchange.swapAllArtifacts" : "Vyměnit artefakt",
|
||||
"vcmi.quickExchange.swapAllArtifacts" : "Vyměnit artefakty",
|
||||
|
||||
"vcmi.radialWheel.mergeSameUnit" : "Sloučit stejné jednotky",
|
||||
"vcmi.radialWheel.fillSingleUnit" : "Vyplnit jednou jednotkou",
|
||||
|
@ -53,7 +53,7 @@
|
||||
"vcmi.quickExchange.moveAllUnits" : "Move All Units",
|
||||
"vcmi.quickExchange.swapAllUnits" : "Swap Armies",
|
||||
"vcmi.quickExchange.moveAllArtifacts" : "Move All Artifacts",
|
||||
"vcmi.quickExchange.swapAllArtifacts" : "Swap Artifact",
|
||||
"vcmi.quickExchange.swapAllArtifacts" : "Swap Artifacts",
|
||||
|
||||
"vcmi.radialWheel.mergeSameUnit" : "Merge same creatures",
|
||||
"vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures",
|
||||
|
@ -121,7 +121,43 @@
|
||||
"vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?",
|
||||
"vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück",
|
||||
|
||||
"vcmi.broadcast.failedLoadGame" : "Spiel konnte nicht geladen werden",
|
||||
"vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten",
|
||||
"vcmi.broadcast.simturn.end" : "Simultane Züge wurden beendet",
|
||||
"vcmi.broadcast.simturn.endBetween" : "Simultane Züge zwischen den Spielern %s und %s wurden beendet",
|
||||
"vcmi.broadcast.serverProblem" : "Server hat ein Problem festgestellt",
|
||||
"vcmi.broadcast.gameTerminated" : "Spiel wurde abgebrochen",
|
||||
"vcmi.broadcast.gameSavedAs" : "Spiel gespeichert als",
|
||||
"vcmi.broadcast.noCheater" : "Keine Betrüger registriert!",
|
||||
"vcmi.broadcast.playerCheater" : "Spieler %s ist ein Betrüger!",
|
||||
"vcmi.broadcast.statisticFile" : "Die Statistikdateien befinden sich im Verzeichnis %s",
|
||||
"vcmi.broadcast.help.commands" : "Verfügbare Befehle für den Host:",
|
||||
"vcmi.broadcast.help.exit" : "'!exit' - beendet sofort das aktuelle Spiel",
|
||||
"vcmi.broadcast.help.kick" : "'!kick <player>' - den angegebenen Spieler aus dem Spiel werfen",
|
||||
"vcmi.broadcast.help.save" : "'!save <filename>' - Spiel unter dem angegebenen Dateinamen speichern",
|
||||
"vcmi.broadcast.help.statistic" : "'!statistic' - Spielstatistiken als csv-Datei speichern",
|
||||
"vcmi.broadcast.help.commandsAll" : "Verfügbare Befehle für alle Spieler:",
|
||||
"vcmi.broadcast.help.help" : "'!help' - diese Hilfe anzeigen",
|
||||
"vcmi.broadcast.help.cheaters" : "'!cheaters' - Liste der Spieler, die während des Spiels einen Cheat-Befehl eingegeben haben",
|
||||
"vcmi.broadcast.help.vote" : "'!vote' - erlaubt es, einige Spieleinstellungen zu ändern, wenn alle Spieler dafür stimmen",
|
||||
"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - erlaubt simultane Züge für eine bestimmte Anzahl von Tagen oder bis zum Kontakt",
|
||||
"vcmi.broadcast.vote.force" : "'!vote simturns force X' - erzwingt simultane Züge für die angegebene Anzahl von Tagen und blockiert Spielerkontakte",
|
||||
"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - Simultane Züge abbrechen, sobald dieser Zug endet",
|
||||
"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - verlängert den Basis-Timer für alle Spieler um die angegebene Anzahl von Sekunden",
|
||||
"vcmi.broadcast.vote.noActive" : "Keine aktive Abstimmung!",
|
||||
"vcmi.broadcast.vote.yes" : "ja",
|
||||
"vcmi.broadcast.vote.no" : "nein",
|
||||
"vcmi.broadcast.vote.notRecognized" : "Abstimmungsbefehl nicht erkannt!",
|
||||
"vcmi.broadcast.vote.success.untilContacts" : "Abstimmung erfolgreich. Simultane Züge laufen für %s weitere Tage, oder bis zum Kontakt",
|
||||
"vcmi.broadcast.vote.success.contactsBlocked" : "Abstimmung erfolgreich. Simultane Züge werden für %s weitere Tage laufen. Kontakte sind blockiert",
|
||||
"vcmi.broadcast.vote.success.nextDay" : "Abstimmung erfolgreich. Simultane Züge werden am nächsten Tag beendet",
|
||||
"vcmi.broadcast.vote.success.timer" : "Abstimmung erfolgreich. Der Timer für alle Spieler wurde um %s Sekunden verlängert.",
|
||||
"vcmi.broadcast.vote.aborted" : "Spieler haben gegen die Änderung gestimmt. Abstimmung abgebrochen",
|
||||
"vcmi.broadcast.vote.start.untilContacts" : "Abstimmung gestartet, um simultane Züge für weitere %s Tage zu erlauben",
|
||||
"vcmi.broadcast.vote.start.contactsBlocked" : "Abstimmung über die Erzwingung simultaner Züge für weitere %s-Tage eingeleitet",
|
||||
"vcmi.broadcast.vote.start.nextDay" : "Beginn der Abstimmung zur Beendigung der simultanen Züge ab dem nächsten Tag",
|
||||
"vcmi.broadcast.vote.start.timer" : "Abstimmung gestartet, um den Timer für alle Spieler um %s Sekunden zu verlängern",
|
||||
"vcmi.broadcast.vote.hint" : "Gib '!vote yes' ein, um dieser Änderung zuzustimmen oder '!vote no', um dagegen zu stimmen",
|
||||
|
||||
"vcmi.lobby.login.title" : "VCMI Online Lobby",
|
||||
"vcmi.lobby.login.username" : "Benutzername:",
|
||||
@ -130,6 +166,7 @@
|
||||
"vcmi.lobby.login.create" : "Neuer Account",
|
||||
"vcmi.lobby.login.login" : "Login",
|
||||
"vcmi.lobby.login.as" : "Login als %s",
|
||||
"vcmi.lobby.login.spectator" : "Beobachter",
|
||||
"vcmi.lobby.header.rooms" : "Spielräume - %d",
|
||||
"vcmi.lobby.header.channels" : "Chat Kanäle",
|
||||
"vcmi.lobby.header.chat.global" : "Globaler Spiele-Chat - %s", // %s -> language name
|
||||
@ -190,8 +227,9 @@
|
||||
"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.modDependencyLoop" : "Mod {'%s'} konnte nicht geladen werden.!\n Möglicherweise befindet sie sich in einer (weichen) Abhängigkeitsschleife.",
|
||||
"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.server.errors.wrongIdentified" : "Ihr wurdet als Spieler %s identifiziert, während %s erwartet wurde",
|
||||
"vcmi.server.errors.notAllowed" : "Ihr dürft diese Aktion nicht durchführen!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "Es ist nicht möglich, mit einer Dimensionstür vom Meer zum Land oder umgekehrt zu teleportieren.",
|
||||
|
||||
|
@ -12,9 +12,9 @@
|
||||
"vcmi.adventureMap.monsterThreat.levels.9" : "Przytłaczający",
|
||||
"vcmi.adventureMap.monsterThreat.levels.10" : "Śmiertelny",
|
||||
"vcmi.adventureMap.monsterThreat.levels.11" : "Nie do pokonania",
|
||||
"vcmi.adventureMap.monsterLevel" : "\n\nJednostka %ATTACK_TYPE %LEVEL poziomu z miasta %TOWN",
|
||||
"vcmi.adventureMap.monsterMeleeType" : "Walcząca wręcz",
|
||||
"vcmi.adventureMap.monsterRangedType" : "Dystansowa",
|
||||
"vcmi.adventureMap.monsterLevel" : "\n\nJednostka %ATTACK_TYPE %LEVEL-go poziomu z miasta %TOWN",
|
||||
"vcmi.adventureMap.monsterMeleeType" : "walcząca wręcz",
|
||||
"vcmi.adventureMap.monsterRangedType" : "dystansowa",
|
||||
"vcmi.adventureMap.search.hover" : "Wyszukiwarka obiektów",
|
||||
"vcmi.adventureMap.search.help" : "Wybierz obiekt który chcesz znaleźć na mapie.",
|
||||
|
||||
@ -35,6 +35,44 @@
|
||||
"vcmi.bonusSource.commander" : "Dowódca",
|
||||
"vcmi.bonusSource.other" : "Inne",
|
||||
|
||||
"vcmi.broadcast.command" : "Wpisz '!help' aby zobaczyć listę dostępnych komend.",
|
||||
"vcmi.broadcast.failedLoadGame" : "Nie udało się wczytać gry",
|
||||
"vcmi.broadcast.gameSavedAs" : "gra zapisana jako",
|
||||
"vcmi.broadcast.gameTerminated" : "gra została zamknięta",
|
||||
"vcmi.broadcast.help.cheaters" : "'!cheaters' - wyświetla listę graczy, którzy użyli 'kodów' w trakcie gry",
|
||||
"vcmi.broadcast.help.commands" : "Dostępne komendy dla hosta:",
|
||||
"vcmi.broadcast.help.commandsAll" : "Dostępne komendy dla wszystkich graczy:",
|
||||
"vcmi.broadcast.help.exit" : "'!exit' - natychmiast kończy bieżącą grę",
|
||||
"vcmi.broadcast.help.help" : "'!help' - wyświetla tę pomoc",
|
||||
"vcmi.broadcast.help.kick" : "'!kick <player>' - wyrzuca określonego gracza z gry",
|
||||
"vcmi.broadcast.help.save" : "'!save <filename>' - zapisz grę pod określoną nazwą",
|
||||
"vcmi.broadcast.help.statistic" : "'!statistic' - zapisz statystyki gry do pliku csv",
|
||||
"vcmi.broadcast.help.vote" : "'!vote' - pozwala na zmianę ustawień gry jeśli wszyscy gracze zagłosują 'za'",
|
||||
"vcmi.broadcast.noCheater" : "Nie zarejestrowano oszustw!",
|
||||
"vcmi.broadcast.playerCheater" : "Gracz %s to oszust!",
|
||||
"vcmi.broadcast.serverProblem" : "Serwer napotkał problem",
|
||||
"vcmi.broadcast.simturn.end" : "Tury symultaniczne zostały zakończone",
|
||||
"vcmi.broadcast.simturn.endBetween" : "Tury symultaniczne pomiędzy graczami %s i %s dobiegły końca",
|
||||
"vcmi.broadcast.statisticFile" : "Pliki statystyk są dostępne w folderze %s",
|
||||
"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - przerwanie tur symultanicznych po zakończeniu tej tury",
|
||||
"vcmi.broadcast.vote.aborted" : "Gracz zagłosował przeciwko. Głosowanie anulowane.",
|
||||
"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - tury symultaniczne przez określoną ilość dni, albo do pierwszego kontaktu",
|
||||
"vcmi.broadcast.vote.force" : "'!vote simturns force X' - tury symultaniczne przez określoną ilość dni, niezależnie od kontaktu",
|
||||
"vcmi.broadcast.vote.hint" : "Wpisz '!vote yes' żeby zagłosować na tak lub '!vote no' - przeciwko tej zmianie",
|
||||
"vcmi.broadcast.vote.no" : "nie",
|
||||
"vcmi.broadcast.vote.noActive" : "Żadne głosowanie nie jest aktywne!",
|
||||
"vcmi.broadcast.vote.notRecognized" : "Nieznana komenda do głosowania!",
|
||||
"vcmi.broadcast.vote.start.contactsBlocked" : "Rozpoczęto głosowanie: tury symultaniczne przez %s kolejnych dni",
|
||||
"vcmi.broadcast.vote.start.nextDay" : "Rozpoczęto głosowanie: zakończenie tur symultanicznych od następnego dnia",
|
||||
"vcmi.broadcast.vote.start.timer" : "Rozpoczęto głosowanie: wydłużenie czasu dla wszystkich graczy o %s sekund",
|
||||
"vcmi.broadcast.vote.start.untilContacts" : "Rozpoczęto głosowanie: przedłużenie trwania tur symultanicznych o kolejne %s dni",
|
||||
"vcmi.broadcast.vote.success.contactsBlocked" : "Głosowanie zakończone pomyślnie. Tury symultaniczne będą trwały jeszcze przez kolejne %s dni, niezależnie od kontaktu.",
|
||||
"vcmi.broadcast.vote.success.nextDay" : "Głosowanie zakończone pomyślnie. Tury symultaniczne zostaną wyłączone w kolejnym dniu",
|
||||
"vcmi.broadcast.vote.success.timer" : "Głosowanie zakończone pomyślnie. Czas dla wszystkich graczy został wydłużony o %s sekund",
|
||||
"vcmi.broadcast.vote.success.untilContacts" : "Głosowanie zakończone pomyślnie. Tury symultaniczne będą trwały jeszcze przez kolejne %s dni lub do pierwszego kontaktu.",
|
||||
"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - wydłużenie bazowego czasu dla wszystkich graczy o X sekund",
|
||||
"vcmi.broadcast.vote.yes" : "tak",
|
||||
|
||||
"vcmi.capitalColors.0" : "Czerwony",
|
||||
"vcmi.capitalColors.1" : "Niebieski",
|
||||
"vcmi.capitalColors.2" : "Brązowy",
|
||||
@ -174,6 +212,12 @@
|
||||
"vcmi.lobby.pvp.randomTownVs.hover" : "Wylosuj 2 miasta",
|
||||
"vcmi.lobby.pvp.randomTownVs.help" : "Wyświetli nazwę 2 wylosowanych miast na czacie, które nie zostały zablokowane na liście",
|
||||
"vcmi.lobby.pvp.versus" : "vs.",
|
||||
"vcmi.lobby.deleteFile" : "Czy chcesz usunąć ten plik ?",
|
||||
"vcmi.lobby.deleteFolder" : "Czy chcesz usunąć ten folder ?",
|
||||
"vcmi.lobby.deleteMapTitle" : "Wskaż tytuł, który chcesz usunąć",
|
||||
"vcmi.lobby.deleteMode" : "Przełącza tryb na usuwanie i spowrotem",
|
||||
"vcmi.lobby.deleteSaveGameTitle" : "Wskaż zapis gry do usunięcia",
|
||||
"vcmi.lobby.deleteUnsupportedSave" : "{Znaleziono niekompatybilne zapisy gry}\n\nVCMI wykrył %d zapisów gry, które nie są już wspierane. Prawdopodobnie ze względu na różne wersje gry.\n\nCzy chcesz je usunąć ?",
|
||||
|
||||
"vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s",
|
||||
"vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.",
|
||||
@ -183,7 +227,9 @@
|
||||
"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.modDependencyLoop" : "Nie udało się wczytać moda {'%s'}!\n Być może znajduje się w pętli zależności",
|
||||
"vcmi.server.errors.notAllowed" : "To działanie nie jest dozwolone!",
|
||||
"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.server.errors.wrongIdentified" : "Zostałeś zidentyfikowany jako gracz %s natomiast powinieneś być %s",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "Nie jest możliwa teleportacja przez drzwi wymiarów z wód na ląd i na odwrót.",
|
||||
|
||||
@ -228,7 +274,7 @@
|
||||
"vcmi.statisticWindow.param.obeliskVisited" : "Lb. obelisków",
|
||||
"vcmi.statisticWindow.icon.townCaptured" : "Miasto zdobyte",
|
||||
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Najsilniejszy bohater przeciwnika pokonany",
|
||||
"vcmi.statisticWindow.icon.grailFound" : "Gral znaleziony",
|
||||
"vcmi.statisticWindow.icon.grailFound" : "Graal znaleziony",
|
||||
"vcmi.statisticWindow.icon.defeated" : "Pokonany",
|
||||
|
||||
"vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)",
|
||||
@ -491,11 +537,33 @@
|
||||
"vcmi.stackExperience.rank.4" : "Udowodniony",
|
||||
"vcmi.stackExperience.rank.5" : "Weteran",
|
||||
"vcmi.stackExperience.rank.6" : "Adept",
|
||||
"vcmi.stackExperience.rank.7" : "Expert",
|
||||
"vcmi.stackExperience.rank.7" : "Ekspert",
|
||||
"vcmi.stackExperience.rank.8" : "Elitarny",
|
||||
"vcmi.stackExperience.rank.9" : "Master",
|
||||
"vcmi.stackExperience.rank.9" : "Mistrz",
|
||||
"vcmi.stackExperience.rank.10" : "As",
|
||||
|
||||
"spell.core.castleMoat.name" : "Fosa",
|
||||
"spell.core.castleMoatTrigger.name" : "Fosa",
|
||||
"spell.core.catapultShot.name" : "Strzał z katapulty",
|
||||
"spell.core.cyclopsShot.name" : "Strzał oblężniczy",
|
||||
"spell.core.dungeonMoat.name" : "Wrzący olej",
|
||||
"spell.core.dungeonMoatTrigger.name" : "Wrzący olej",
|
||||
"spell.core.fireWallTrigger.name" : "Ściana Ognia",
|
||||
"spell.core.firstAid.name" : "Pierwsza pomoc",
|
||||
"spell.core.fortressMoat.name" : "Wrząca smoła",
|
||||
"spell.core.fortressMoatTrigger.name" : "Wrząca smoła",
|
||||
"spell.core.infernoMoat.name" : "Lawa",
|
||||
"spell.core.infernoMoatTrigger.name" : "Lawa",
|
||||
"spell.core.landMineTrigger.name" : "Mina",
|
||||
"spell.core.necropolisMoat.name" : "Fosa z kości",
|
||||
"spell.core.necropolisMoatTrigger.name" : "Fosa z kości",
|
||||
"spell.core.rampartMoat.name" : "Ciernisko",
|
||||
"spell.core.rampartMoatTrigger.name" : "Ciernisko",
|
||||
"spell.core.strongholdMoat.name" : "Palisada obronna",
|
||||
"spell.core.strongholdMoatTrigger.name" : "Palisada obronna",
|
||||
"spell.core.summonDemons.name" : "Przyzwanie Demonów",
|
||||
"spell.core.towerMoat.name" : "Pole minowe",
|
||||
|
||||
// Strings for HotA Seer Hut / Quest Guards
|
||||
"core.seerhut.quest.heroClass.complete.0" : "Ah, ty jesteś %s. Oto prezent dla ciebie. Czy go przyjmiesz?",
|
||||
"core.seerhut.quest.heroClass.complete.1" : "Ah, ty jesteś %s. Oto prezent dla ciebie. Czy go przyjmiesz?",
|
||||
|
@ -12,6 +12,11 @@
|
||||
"vcmi.adventureMap.monsterThreat.levels.9" : "Нездоланна",
|
||||
"vcmi.adventureMap.monsterThreat.levels.10" : "Смертельна",
|
||||
"vcmi.adventureMap.monsterThreat.levels.11" : "Неможлива",
|
||||
"vcmi.adventureMap.monsterLevel" : "\n\n%TOWN, істота%ATTACK_TYPE, %LEVELго рівня",
|
||||
"vcmi.adventureMap.monsterMeleeType" : " ближнього бою",
|
||||
"vcmi.adventureMap.monsterRangedType" : "-стрілок",
|
||||
"vcmi.adventureMap.search.hover" : "Шукати об'єкт мапи",
|
||||
"vcmi.adventureMap.search.help" : "Оберіть об'єкт для пошуку на мапі.",
|
||||
|
||||
"vcmi.adventureMap.confirmRestartGame" : "Ви впевнені, що хочете перезапустити гру?",
|
||||
"vcmi.adventureMap.noTownWithMarket" : "Немає доступних ринків!",
|
||||
@ -20,8 +25,16 @@
|
||||
"vcmi.adventureMap.playerAttacked" : "Гравця атаковано: %s",
|
||||
"vcmi.adventureMap.moveCostDetails" : "Очки руху - Вартість: %TURNS ходів + %POINTS очок. Залишок очок: %REMAINING",
|
||||
"vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки руху - Вартість: %POINTS очок, Залишок очок: %REMAINING",
|
||||
"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" : "Сірий",
|
||||
@ -36,6 +49,12 @@
|
||||
"vcmi.heroOverview.secondarySkills" : "Навички",
|
||||
"vcmi.heroOverview.spells" : "Закляття",
|
||||
|
||||
"vcmi.quickExchange.moveUnit" : "Перемістити загін",
|
||||
"vcmi.quickExchange.moveAllUnits" : "Перемістити усі загони",
|
||||
"vcmi.quickExchange.swapAllUnits" : "Обміняти армії",
|
||||
"vcmi.quickExchange.moveAllArtifacts" : "Перемістити усі артефакти",
|
||||
"vcmi.quickExchange.swapAllArtifacts" : "Обміняти усі артефакти",
|
||||
|
||||
"vcmi.radialWheel.mergeSameUnit" : "Об'єднати однакових істот",
|
||||
"vcmi.radialWheel.fillSingleUnit" : "Заповнити одиничними істотами",
|
||||
"vcmi.radialWheel.splitSingleUnit" : "Відділити одну істоту",
|
||||
@ -55,8 +74,26 @@
|
||||
"vcmi.radialWheel.moveDown" : "Перемістити вниз",
|
||||
"vcmi.radialWheel.moveBottom" : "Перемістити у кінець",
|
||||
|
||||
"vcmi.randomMap.description" : "Мапа створена генератором випадкових мап.\nШаблон був %s, розмір %dx%d, рівнів %d, гравців %d, гравців-ШІ %d, %s, %s монстри, мапа VCMI",
|
||||
"vcmi.randomMap.description.isHuman" : ", %s людина",
|
||||
"vcmi.randomMap.description.townChoice" : ", %s обрав замок %s",
|
||||
"vcmi.randomMap.description.water.none" : "води немає",
|
||||
"vcmi.randomMap.description.water.normal" : "вода присутня",
|
||||
"vcmi.randomMap.description.water.islands" : "острівна",
|
||||
"vcmi.randomMap.description.monster.weak" : "слабкі",
|
||||
"vcmi.randomMap.description.monster.normal" : "звичайні",
|
||||
"vcmi.randomMap.description.monster.strong" : "сильні",
|
||||
|
||||
"vcmi.spellBook.search" : "шукати...",
|
||||
|
||||
"vcmi.spellResearch.canNotAfford" : "Ви не можете дозволити собі замінити {%SPELL1} на {%SPELL2}. Але ви все одно можете відкинути це закляття і продовжити дослідження заклять.",
|
||||
"vcmi.spellResearch.comeAgain" : "Сьогодні дослідження вже зроблено. Приходьте завтра..",
|
||||
"vcmi.spellResearch.pay" : "Ви хочете замінити {%SPELL1} на {%SPELL2}? Або відкинути це закляття і продовжити дослідження заклять??",
|
||||
"vcmi.spellResearch.research" : "Дослідити це закляття",
|
||||
"vcmi.spellResearch.skip" : "Пропустити це закляття",
|
||||
"vcmi.spellResearch.abort" : "Припинити",
|
||||
"vcmi.spellResearch.noMoreSpells" : "Більше немає жодного закляття, доступного для дослідження.",
|
||||
|
||||
"vcmi.mainMenu.serverConnecting" : "Підключення...",
|
||||
"vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:",
|
||||
"vcmi.mainMenu.serverConnectionFailed" : "Помилка з'єднання",
|
||||
@ -73,6 +110,56 @@
|
||||
"vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни",
|
||||
"vcmi.lobby.backToLobby" : "Назад до лобі",
|
||||
|
||||
"vcmi.lobby.author" : "Автор",
|
||||
"vcmi.lobby.handicap" : "Гандикап",
|
||||
"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.broadcast.failedLoadGame" : "Не вдалося завантажити гру",
|
||||
"vcmi.broadcast.command" : "Введіть '!help' у чаті гри, щоб переглянути список доступних команд",
|
||||
"vcmi.broadcast.simturn.end" : "Одночасні ходи закінчилися",
|
||||
"vcmi.broadcast.simturn.endBetween" : "Одночасні ходи між гравцями %s та %s завершилися",
|
||||
"vcmi.broadcast.serverProblem" : "Сервер зіткнувся з проблемою",
|
||||
"vcmi.broadcast.gameTerminated" : "гру було завершено",
|
||||
"vcmi.broadcast.gameSavedAs" : "гру збережено як",
|
||||
"vcmi.broadcast.noCheater" : "Читерів не зареєстровано!",
|
||||
"vcmi.broadcast.playerCheater" : "Гравець %s - шахрай!",
|
||||
"vcmi.broadcast.statisticFile" : "Файли статистики можна знайти в каталозі %s",
|
||||
"vcmi.broadcast.help.commands" : "Команди доступні для хоста:",
|
||||
"vcmi.broadcast.help.exit" : "'!exit' - негайно завершує поточну гру",
|
||||
"vcmi.broadcast.help.kick" : "'!kick <player>' - вигнати вказаного гравця з гри",
|
||||
"vcmi.broadcast.help.save" : "'!save <filename>' - зберегти гру під вказаним ім'ям",
|
||||
"vcmi.broadcast.help.statistic" : "'!statistic' - зберегти статистику гри у форматі csv",
|
||||
"vcmi.broadcast.help.commandsAll" : "Команди доступні всім гравцям:",
|
||||
"vcmi.broadcast.help.help" : "'!help' - відобразити цю довідку",
|
||||
"vcmi.broadcast.help.cheaters" : "'!cheaters' - список гравців, які вводили чит-команду під час гри",
|
||||
"vcmi.broadcast.help.vote" : "'!vote' - дозволяє змінити деякі налаштування гри, якщо всі гравці проголосують за це",
|
||||
"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - дозволяти одночасні ходи на визначену кількість днів або до контакту",
|
||||
"vcmi.broadcast.vote.force" : "'!vote simturns force X' - увімкнути одночасні ходи на визначену кількість днів, блокуючи контакти гравців",
|
||||
"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - завершити одночасні ходи, як тільки цей хід закінчиться",
|
||||
"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - подовжити базовий таймер для всіх гравців на вказану кількість секунд",
|
||||
"vcmi.broadcast.vote.noActive" : "Активне голосування відсутнє!",
|
||||
"vcmi.broadcast.vote.yes" : "так",
|
||||
"vcmi.broadcast.vote.no" : "ні",
|
||||
"vcmi.broadcast.vote.notRecognized" : "Команда для голосування не розпізнана!",
|
||||
"vcmi.broadcast.vote.success.untilContacts" : "Голосування пройшло успішно. Одночасні ходи триватимуть ще %s днів, або до контакту",
|
||||
"vcmi.broadcast.vote.success.contactsBlocked" : "Голосування пройшло успішно. Одночасні ходи триватимуть ще %s днів. Контакти між гравцями заблоковані",
|
||||
"vcmi.broadcast.vote.success.nextDay" : "Голосування пройшло успішно. Одночасні ходи закінчаться на наступний день",
|
||||
"vcmi.broadcast.vote.success.timer" : "Голосування пройшло успішно. Таймер для всіх гравців було подовжено на %s секунд",
|
||||
"vcmi.broadcast.vote.aborted" : "Гравець проголосував проти змін. Голосування перервано",
|
||||
"vcmi.broadcast.vote.start.untilContacts" : "Розпочато голосування, за одночасні ходи на %s більше днів або до контакту гравців",
|
||||
"vcmi.broadcast.vote.start.contactsBlocked" : "Розпочато голосування за безумовні одночасні ходи на %s більше днів",
|
||||
"vcmi.broadcast.vote.start.nextDay" : "Розпочато голосування за припинення одночасних ходів з наступного дня",
|
||||
"vcmi.broadcast.vote.start.timer" : "Розпочато голосування за продовження таймера для всіх гравців на %s секунд",
|
||||
"vcmi.broadcast.vote.hint" : "Введіть \"!vote yes\", щоб погодитися з цією зміною, або \"!vote no\", щоб проголосувати проти неї",
|
||||
|
||||
"vcmi.lobby.login.title" : "Онлайн лобі VCMI",
|
||||
"vcmi.lobby.login.username" : "Логін:",
|
||||
"vcmi.lobby.login.connecting" : "Підключення...",
|
||||
@ -80,6 +167,7 @@
|
||||
"vcmi.lobby.login.create" : "Створити акаунт",
|
||||
"vcmi.lobby.login.login" : "Увійти",
|
||||
"vcmi.lobby.login.as" : "Увійти як %s",
|
||||
"vcmi.lobby.login.spectator" : "Спостерігач",
|
||||
"vcmi.lobby.header.rooms" : "Активні кімнати - %d",
|
||||
"vcmi.lobby.header.channels" : "Канали чату",
|
||||
"vcmi.lobby.header.chat.global" : "Глобальний ігровий чат - %s", // %s -> language name
|
||||
@ -135,11 +223,14 @@
|
||||
|
||||
"vcmi.client.errors.invalidMap" : "{Пошкоджена карта або кампанія}\n\nНе вдалося запустити гру! Вибрана карта або кампанія може бути невірною або пошкодженою. Причина:\n%s",
|
||||
"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
|
||||
"vcmi.server.errors.disconnected" : "{Помилка мережі}\n\nВтрачено зв'язок з сервером гри!",
|
||||
"vcmi.server.errors.playerLeft" : "{Гравець покинув гру}\n\n%s гравець від'єднався від гри!", //%s -> player color
|
||||
"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
|
||||
"vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}",
|
||||
"vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}",
|
||||
"vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?",
|
||||
"vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!",
|
||||
"vcmi.server.errors.wrongIdentified" : "Ви були ідентифіковані як гравець %s, хоча очікували %s",
|
||||
"vcmi.server.errors.notAllowed" : "Ви не можете виконати цю дію!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами",
|
||||
|
||||
@ -155,6 +246,38 @@
|
||||
"vcmi.systemOptions.otherGroup" : "Інші налаштування",
|
||||
"vcmi.systemOptions.townsGroup" : "Екран міста",
|
||||
|
||||
"vcmi.statisticWindow.statistics" : "Статистика",
|
||||
"vcmi.statisticWindow.tsvCopy" : "Дані до буфера обміну",
|
||||
"vcmi.statisticWindow.selectView" : "Оберіть представлення",
|
||||
"vcmi.statisticWindow.value" : "Цінність",
|
||||
"vcmi.statisticWindow.title.overview" : "Загальний огляд",
|
||||
"vcmi.statisticWindow.title.resources" : "Ресурси",
|
||||
"vcmi.statisticWindow.title.income" : "Прибуток",
|
||||
"vcmi.statisticWindow.title.numberOfHeroes" : "К-сть героїв",
|
||||
"vcmi.statisticWindow.title.numberOfTowns" : "К-сть міст",
|
||||
"vcmi.statisticWindow.title.numberOfArtifacts" : "К-сть артефактів",
|
||||
"vcmi.statisticWindow.title.numberOfDwellings" : "К-сть помешкань",
|
||||
"vcmi.statisticWindow.title.numberOfMines" : "К-сть шахт",
|
||||
"vcmi.statisticWindow.title.armyStrength" : "Сила армії",
|
||||
"vcmi.statisticWindow.title.experience" : "Досвід",
|
||||
"vcmi.statisticWindow.title.resourcesSpentArmy" : "Витрати на армію",
|
||||
"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Витрати на будівництво",
|
||||
"vcmi.statisticWindow.title.mapExplored" : "Ступінь вивченості карти",
|
||||
"vcmi.statisticWindow.param.playerName" : "Ім'я гравця",
|
||||
"vcmi.statisticWindow.param.daysSurvived" : "Прожиті дні",
|
||||
"vcmi.statisticWindow.param.maxHeroLevel" : "Макс. рівень героя",
|
||||
"vcmi.statisticWindow.param.battleWinRatioHero" : "Частка перемог (проти героя)",
|
||||
"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Частка перемог (проти нейтральних)",
|
||||
"vcmi.statisticWindow.param.battlesHero" : "Боїв (проти героя)",
|
||||
"vcmi.statisticWindow.param.battlesNeutral" : "Боїв (проти нейтральних)",
|
||||
"vcmi.statisticWindow.param.maxArmyStrength" : "Макс. сила армії",
|
||||
"vcmi.statisticWindow.param.tradeVolume" : "Обсяг торгівлі",
|
||||
"vcmi.statisticWindow.param.obeliskVisited" : "Відвідано обеліск",
|
||||
"vcmi.statisticWindow.icon.townCaptured" : "Місто захоплено",
|
||||
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Перемогли сильного героя суперника",
|
||||
"vcmi.statisticWindow.icon.grailFound" : "Грааль знайдено",
|
||||
"vcmi.statisticWindow.icon.defeated" : "Переможений",
|
||||
|
||||
"vcmi.systemOptions.fullscreenBorderless.hover" : "На весь екран (безрамкове вікно)",
|
||||
"vcmi.systemOptions.fullscreenBorderless.help" : "{На весь екран (безрамкове вікно)}\n\nЯкщо обрано, VCMI працюватиме у режимі безрамкового вікна на весь екран. У цьому режимі гра завжди використовує ту саму роздільну здатність, що й робочий стіл, ігноруючи вибрану роздільну здатність",
|
||||
"vcmi.systemOptions.fullscreenExclusive.hover" : "На весь екран (ексклюзивний режим)",
|
||||
@ -195,8 +318,10 @@
|
||||
"vcmi.adventureOptions.borderScroll.help" : "{{Прокрутка по краю}\n\nПрокручувати мапу пригод, коли курсор знаходиться біля краю вікна. Цю функцію можна вимкнути, утримуючи клавішу CTRL.",
|
||||
"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Керування істотами у вікні статусу",
|
||||
"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Керування істотами у вікні статусу}\n\nДозволяє впорядковувати істот у вікні статусу замість циклічного перемикання між типовими компонентами",
|
||||
"vcmi.adventureOptions.leftButtonDrag.hover" : "Переміщення мапи лівою кнопкою",
|
||||
"vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи лівою кнопкою}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод",
|
||||
"vcmi.adventureOptions.leftButtonDrag.hover" : "Переміщення мапи ЛКМ",
|
||||
"vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи ЛКМ}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод",
|
||||
"vcmi.adventureOptions.rightButtonDrag.hover" : "Переміщення мапи ПКМ",
|
||||
"vcmi.adventureOptions.rightButtonDrag.help" : "{Переміщення мапи ПКМ}\n\nЯкщо увімкнено, переміщення миші з натиснутою правою кнопкою буде перетягувати мапу пригод",
|
||||
"vcmi.adventureOptions.smoothDragging.hover" : "Плавне перетягування мапи",
|
||||
"vcmi.adventureOptions.smoothDragging.help" : "{Плавне перетягування мапи}\n\nЯкщо увімкнено, перетягування мапи має сучасний ефект завершення.",
|
||||
"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Вимкнути ефекти зникнення",
|
||||
@ -235,6 +360,8 @@
|
||||
"vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.",
|
||||
"vcmi.battleOptions.endWithAutocombat.hover": "Завершує бій",
|
||||
"vcmi.battleOptions.endWithAutocombat.help": "{Завершує бій}\n\nАвто-бій миттєво завершує бій",
|
||||
"vcmi.battleOptions.showQuickSpell.hover": "Панель швидкого чарування",
|
||||
"vcmi.battleOptions.showQuickSpell.help": "{Панель швидкого чарування}\n\nПоказати панель для швидкого вибору заклять.",
|
||||
|
||||
"vcmi.adventureMap.revisitObject.hover" : "Відвідати Об'єкт",
|
||||
"vcmi.adventureMap.revisitObject.help" : "{Відвідати Об'єкт}\n\nЯкщо герой в даний момент стоїть на об'єкті мапи, він може знову відвідати цю локацію.",
|
||||
@ -284,6 +411,9 @@
|
||||
"vcmi.townHall.missingBase" : "Спочатку необхідно звести початкову будівлю: %s",
|
||||
"vcmi.townHall.noCreaturesToRecruit" : "Немає істот, яких можна завербувати!",
|
||||
|
||||
"vcmi.townStructure.bank.borrow" : "Ви заходите в банк. Вас бачить банкір і каже: 'Ми зробили для вас спеціальну пропозицію. Ви можете взяти у нас позику в розмірі 2500 золотих на 5 днів. Але щодня ви повинні будете повертати по 500 золотих'.",
|
||||
"vcmi.townStructure.bank.payBack" : "Ви заходите в банк. Банкір бачить вас і каже: 'Ви вже отримали позику. Погасіть її, перш ніж брати нову позику'.",
|
||||
|
||||
"vcmi.logicalExpressions.anyOf" : "Будь-що з перерахованого:",
|
||||
"vcmi.logicalExpressions.allOf" : "Все з перерахованого:",
|
||||
"vcmi.logicalExpressions.noneOf" : "Нічого з перерахованого:",
|
||||
@ -292,6 +422,13 @@
|
||||
"vcmi.heroWindow.openCommander.help" : "Показує інформацію про командира героя",
|
||||
"vcmi.heroWindow.openBackpack.hover" : "Відкрити вікно рюкзака з артефактами",
|
||||
"vcmi.heroWindow.openBackpack.help" : "Відкриває вікно, що дозволяє легше керувати рюкзаком артефактів",
|
||||
"vcmi.heroWindow.sortBackpackByCost.hover" : "Сортувати за вартістю",
|
||||
"vcmi.heroWindow.sortBackpackByCost.help" : "Сортувати артефакти в рюкзаку за вартістю.",
|
||||
"vcmi.heroWindow.sortBackpackBySlot.hover" : "Сортувати за типом",
|
||||
"vcmi.heroWindow.sortBackpackBySlot.help" : "Сортувати артефакти в рюкзаку за слотом, в який цей артефакт може бути екіпірований",
|
||||
"vcmi.heroWindow.sortBackpackByClass.hover" : "Сортування за рідкістю",
|
||||
"vcmi.heroWindow.sortBackpackByClass.help" : "Сортувати артефакти в рюкзаку за класом рідкісності артефакту. Скарб, Малий, Великий, Реліквія",
|
||||
"vcmi.heroWindow.fusingArtifact.fusing" : "Ви володієте всіма компонентами, необхідними для злиття %s. Ви бажаєте виконати злиття? {Всі компоненти буде спожито під час злиття.}",
|
||||
|
||||
"vcmi.tavernWindow.inviteHero" : "Запросити героя",
|
||||
|
||||
@ -469,6 +606,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" : "Додаткові відплати",
|
||||
@ -610,5 +749,19 @@
|
||||
"core.bonus.WIDE_BREATH.name" : "Широкий подих",
|
||||
"core.bonus.WIDE_BREATH.description" : "Атака широким подихом",
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Обмежена дальність стрільби",
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів"
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів",
|
||||
"core.bonus.DISINTEGRATE.description" : "Після смерті не залишається трупа",
|
||||
"core.bonus.DISINTEGRATE.name" : "Розпад",
|
||||
"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "При атаці ігнорується ${val}% атаки нападника",
|
||||
"core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ігнорування атаки (${val}%)",
|
||||
"core.bonus.FEROCITY.description" : "Атакує ${val} більше разів, якщо вбиває когось",
|
||||
"core.bonus.FEROCITY.name" : "Лютість",
|
||||
"core.bonus.INVINCIBLE.description" : "На нього ніщо не може вплинути",
|
||||
"core.bonus.INVINCIBLE.name" : "Невразливий",
|
||||
"core.bonus.MECHANICAL.description" : "Імунітет до багатьох ефектів, можна ремонтувати",
|
||||
"core.bonus.MECHANICAL.name" : "Механічний",
|
||||
"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Атака подихом у трьох напрямах",
|
||||
"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Призматична атака",
|
||||
"core.bonus.REVENGE.description" : "Завдає додаткової шкоди залежно від втраченого здоров'я в бою",
|
||||
"core.bonus.REVENGE.name" : "Помста"
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ android {
|
||||
minSdk = qtMinSdkVersion as Integer
|
||||
targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project
|
||||
|
||||
versionCode 1600
|
||||
versionCode 1610
|
||||
versionName "1.6.0"
|
||||
|
||||
setProperty("archivesBaseName", "vcmi")
|
||||
|
@ -430,9 +430,10 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
|
||||
{
|
||||
callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult);
|
||||
|
||||
bool localHumanWinsGame = vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && pack.victoryLossCheckResult.victory();
|
||||
bool lastHumanEndsGame = CSH->howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool();
|
||||
|
||||
if(lastHumanEndsGame)
|
||||
if(lastHumanEndsGame || localHumanWinsGame)
|
||||
{
|
||||
assert(adventureInt);
|
||||
if(adventureInt)
|
||||
|
@ -1064,7 +1064,7 @@ StackQueue::StackBox::StackBox(StackQueue * owner):
|
||||
icon = std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), 0, 0, 9, 1);
|
||||
amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
|
||||
roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 15, 18), ColorRGBA(0, 0, 0, 255), ColorRGBA(241, 216, 120, 255));
|
||||
round = std::make_shared<CLabel>(4, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
|
||||
round = std::make_shared<CLabel>(6, 9, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
|
||||
|
||||
Point iconPos(pos.w - 16, pos.h - 16);
|
||||
|
||||
@ -1105,6 +1105,7 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std::
|
||||
const auto & font = GH.renderHandler().loadFont(FONT_SMALL);
|
||||
int len = font->getStringWidth(tmp);
|
||||
roundRect->pos.w = len + 6;
|
||||
round->pos = Rect(roundRect->pos.center().x, roundRect->pos.center().y, 0, 0);
|
||||
round->setText(tmp);
|
||||
}
|
||||
|
||||
|
@ -32,15 +32,22 @@ InputSourceKeyboard::InputSourceKeyboard()
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string InputSourceKeyboard::getKeyNameWithModifiers(const std::string & keyName) const
|
||||
std::string InputSourceKeyboard::getKeyNameWithModifiers(const std::string & keyName, bool keyUp)
|
||||
{
|
||||
std::string result;
|
||||
|
||||
if (isKeyboardCtrlDown())
|
||||
if(!keyUp)
|
||||
{
|
||||
wasKeyboardCtrlDown = isKeyboardCtrlDown();
|
||||
wasKeyboardAltDown = isKeyboardAltDown();
|
||||
wasKeyboardShiftDown = isKeyboardShiftDown();
|
||||
}
|
||||
|
||||
if (wasKeyboardCtrlDown)
|
||||
result += "Ctrl+";
|
||||
if (isKeyboardAltDown())
|
||||
if (wasKeyboardAltDown)
|
||||
result += "Alt+";
|
||||
if (isKeyboardShiftDown())
|
||||
if (wasKeyboardShiftDown)
|
||||
result += "Shift+";
|
||||
result += keyName;
|
||||
|
||||
@ -49,7 +56,7 @@ std::string InputSourceKeyboard::getKeyNameWithModifiers(const std::string & key
|
||||
|
||||
void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
|
||||
{
|
||||
std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym));
|
||||
std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym), false);
|
||||
logGlobal->trace("keyboard: key '%s' pressed", keyName);
|
||||
assert(key.state == SDL_PRESSED);
|
||||
|
||||
@ -111,7 +118,7 @@ void InputSourceKeyboard::handleEventKeyUp(const SDL_KeyboardEvent & key)
|
||||
if(key.repeat != 0)
|
||||
return; // ignore periodic event resends
|
||||
|
||||
std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym));
|
||||
std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym), true);
|
||||
logGlobal->trace("keyboard: key '%s' released", keyName);
|
||||
|
||||
if (SDL_IsTextInputActive() == SDL_TRUE)
|
||||
|
@ -15,7 +15,11 @@ struct SDL_KeyboardEvent;
|
||||
/// Class that handles keyboard input from SDL events
|
||||
class InputSourceKeyboard
|
||||
{
|
||||
std::string getKeyNameWithModifiers(const std::string & keyName) const;
|
||||
bool wasKeyboardCtrlDown;
|
||||
bool wasKeyboardAltDown;
|
||||
bool wasKeyboardShiftDown;
|
||||
|
||||
std::string getKeyNameWithModifiers(const std::string & keyName, bool keyUp);
|
||||
public:
|
||||
InputSourceKeyboard();
|
||||
|
||||
|
@ -327,6 +327,11 @@ bool CVideoInstance::videoEnded()
|
||||
return getCurrentFrame() == nullptr;
|
||||
}
|
||||
|
||||
CVideoInstance::CVideoInstance()
|
||||
: startTimeInitialized(false), deactivationStartTimeHandling(false)
|
||||
{
|
||||
}
|
||||
|
||||
CVideoInstance::~CVideoInstance()
|
||||
{
|
||||
sws_freeContext(sws);
|
||||
@ -391,8 +396,11 @@ void CVideoInstance::tick(uint32_t msPassed)
|
||||
if(videoEnded())
|
||||
throw std::runtime_error("Video already ended!");
|
||||
|
||||
if(startTime == std::chrono::steady_clock::time_point())
|
||||
if(!startTimeInitialized)
|
||||
{
|
||||
startTime = std::chrono::steady_clock::now();
|
||||
startTimeInitialized = true;
|
||||
}
|
||||
|
||||
auto nowTime = std::chrono::steady_clock::now();
|
||||
double difference = std::chrono::duration_cast<std::chrono::milliseconds>(nowTime - startTime).count() / 1000.0;
|
||||
@ -410,17 +418,18 @@ void CVideoInstance::tick(uint32_t msPassed)
|
||||
|
||||
void CVideoInstance::activate()
|
||||
{
|
||||
if(deactivationStartTime != std::chrono::steady_clock::time_point())
|
||||
if(deactivationStartTimeHandling)
|
||||
{
|
||||
auto pauseDuration = std::chrono::steady_clock::now() - deactivationStartTime;
|
||||
startTime += pauseDuration;
|
||||
deactivationStartTime = std::chrono::steady_clock::time_point();
|
||||
deactivationStartTimeHandling = false;
|
||||
}
|
||||
}
|
||||
|
||||
void CVideoInstance::deactivate()
|
||||
{
|
||||
deactivationStartTime = std::chrono::steady_clock::now();
|
||||
deactivationStartTimeHandling = true;
|
||||
}
|
||||
|
||||
struct FFMpegFormatDescription
|
||||
|
@ -78,6 +78,8 @@ class CVideoInstance final : public IVideoInstance, public FFMpegStream
|
||||
Point dimensions;
|
||||
|
||||
/// video playback start time point
|
||||
bool startTimeInitialized;
|
||||
bool deactivationStartTimeHandling;
|
||||
std::chrono::steady_clock::time_point startTime;
|
||||
std::chrono::steady_clock::time_point deactivationStartTime;
|
||||
|
||||
@ -86,6 +88,7 @@ class CVideoInstance final : public IVideoInstance, public FFMpegStream
|
||||
const int MAX_FRAMESKIP = 5;
|
||||
|
||||
public:
|
||||
CVideoInstance();
|
||||
~CVideoInstance();
|
||||
|
||||
void openVideo();
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
#include "../renderSDL/SDL_Extensions.h"
|
||||
|
||||
#include "../lib/ExceptionsCommon.h"
|
||||
#include "../lib/filesystem/Filesystem.h"
|
||||
#include "../lib/vcmi_endian.h"
|
||||
|
||||
@ -112,6 +113,7 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(const ImagePath & path)
|
||||
|
||||
SDL_Surface * ret=nullptr;
|
||||
|
||||
try {
|
||||
auto readFile = CResourceHandler::get()->load(path)->readAll();
|
||||
|
||||
if (isPCX(readFile.first.get()))
|
||||
@ -146,6 +148,12 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(const ImagePath & path)
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const DataLoadingException & e)
|
||||
{
|
||||
logGlobal->error("%s", e.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// When modifying anything here please check two use cases:
|
||||
// 1) Vampire mansion in Necropolis (not 1st color is transparent)
|
||||
|
@ -111,12 +111,12 @@ public:
|
||||
virtual bool isTransparent(const Point & coords) const = 0;
|
||||
virtual void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0;
|
||||
|
||||
virtual std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const = 0;
|
||||
[[nodiscard]] virtual std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const = 0;
|
||||
|
||||
virtual std::shared_ptr<const ISharedImage> horizontalFlip() const = 0;
|
||||
virtual std::shared_ptr<const ISharedImage> verticalFlip() const = 0;
|
||||
virtual std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const = 0;
|
||||
virtual std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const = 0;
|
||||
[[nodiscard]] virtual std::shared_ptr<const ISharedImage> horizontalFlip() const = 0;
|
||||
[[nodiscard]] virtual std::shared_ptr<const ISharedImage> verticalFlip() const = 0;
|
||||
[[nodiscard]] virtual std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const = 0;
|
||||
[[nodiscard]] virtual std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const = 0;
|
||||
|
||||
|
||||
virtual ~ISharedImage() = default;
|
||||
|
@ -201,7 +201,7 @@ CBitmapFont::CBitmapFont(const std::string & filename):
|
||||
static const std::map<std::string, EScalingAlgorithm> filterNameToEnum = {
|
||||
{ "nearest", EScalingAlgorithm::NEAREST},
|
||||
{ "bilinear", EScalingAlgorithm::BILINEAR},
|
||||
{ "xbrz", EScalingAlgorithm::XBRZ}
|
||||
{ "xbrz", EScalingAlgorithm::XBRZ_ALPHA}
|
||||
};
|
||||
|
||||
auto filterName = settings["video"]["fontUpscalingFilter"].String();
|
||||
|
@ -43,6 +43,9 @@ void ImageScaled::scaleInteger(int factor)
|
||||
|
||||
void ImageScaled::scaleTo(const Point & size)
|
||||
{
|
||||
if (source)
|
||||
source = source->scaleTo(size, nullptr);
|
||||
|
||||
if (body)
|
||||
body = body->scaleTo(size * GH.screenHandler().getScalingFactor(), nullptr);
|
||||
}
|
||||
@ -137,7 +140,6 @@ void ImageScaled::prepareImages()
|
||||
|
||||
switch(blitMode)
|
||||
{
|
||||
case EImageBlitMode::SIMPLE:
|
||||
case EImageBlitMode::WITH_SHADOW:
|
||||
case EImageBlitMode::ONLY_SHADOW:
|
||||
case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
|
||||
|
@ -19,6 +19,8 @@
|
||||
#include "../render/CDefFile.h"
|
||||
#include "../render/Graphics.h"
|
||||
#include "../xBRZ/xbrz.h"
|
||||
#include "../gui/CGuiHandler.h"
|
||||
#include "../render/IScreenHandler.h"
|
||||
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <SDL_surface.h>
|
||||
@ -276,9 +278,15 @@ void SDLImageShared::optimizeSurface()
|
||||
margins.x += left;
|
||||
margins.y += top;
|
||||
}
|
||||
|
||||
if(preScaleFactor > 1 && preScaleFactor != GH.screenHandler().getScalingFactor())
|
||||
{
|
||||
margins.x = margins.x * GH.screenHandler().getScalingFactor() / preScaleFactor;
|
||||
margins.y = margins.y * GH.screenHandler().getScalingFactor() / preScaleFactor;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palette * palette) const
|
||||
std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode mode) const
|
||||
{
|
||||
if (factor <= 0)
|
||||
throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor));
|
||||
@ -293,7 +301,13 @@ std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL
|
||||
if(preScaleFactor == factor)
|
||||
return shared_from_this();
|
||||
else if(preScaleFactor == 1)
|
||||
scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ);
|
||||
{
|
||||
// dump heuristics to differentiate tileable UI elements from map object / combat assets
|
||||
if (mode == EImageBlitMode::OPAQUE || mode == EImageBlitMode::COLORKEY || mode == EImageBlitMode::SIMPLE)
|
||||
scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_OPAQUE);
|
||||
else
|
||||
scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_ALPHA);
|
||||
}
|
||||
else
|
||||
scaled = CSDL_Ext::scaleSurface(surf, (surf->w / preScaleFactor) * factor, (surf->h / preScaleFactor) * factor);
|
||||
|
||||
@ -589,12 +603,12 @@ void SDLImageRGB::scaleTo(const Point & size)
|
||||
|
||||
void SDLImageIndexed::scaleInteger(int factor)
|
||||
{
|
||||
image = image->scaleInteger(factor, currentPalette);
|
||||
image = image->scaleInteger(factor, currentPalette, blitMode);
|
||||
}
|
||||
|
||||
void SDLImageRGB::scaleInteger(int factor)
|
||||
{
|
||||
image = image->scaleInteger(factor, nullptr);
|
||||
image = image->scaleInteger(factor, nullptr, blitMode);
|
||||
}
|
||||
|
||||
void SDLImageRGB::exportBitmap(const boost::filesystem::path & path) const
|
||||
|
@ -57,11 +57,11 @@ public:
|
||||
void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const override;
|
||||
Point dimensions() const override;
|
||||
bool isTransparent(const Point & coords) const override;
|
||||
std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const override;
|
||||
std::shared_ptr<const ISharedImage> horizontalFlip() const override;
|
||||
std::shared_ptr<const ISharedImage> verticalFlip() const override;
|
||||
std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const override;
|
||||
std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const override;
|
||||
[[nodiscard]] std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const override;
|
||||
[[nodiscard]] std::shared_ptr<const ISharedImage> horizontalFlip() const override;
|
||||
[[nodiscard]] std::shared_ptr<const ISharedImage> verticalFlip() const override;
|
||||
[[nodiscard]] std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override;
|
||||
[[nodiscard]] std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const override;
|
||||
|
||||
friend class SDLImageLoader;
|
||||
};
|
||||
|
@ -683,12 +683,17 @@ SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor
|
||||
case EScalingAlgorithm::BILINEAR:
|
||||
xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
|
||||
break;
|
||||
case EScalingAlgorithm::XBRZ:
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate](const tbb::blocked_range<size_t> & r)
|
||||
case EScalingAlgorithm::XBRZ_ALPHA:
|
||||
case EScalingAlgorithm::XBRZ_OPAQUE:
|
||||
{
|
||||
xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, xbrz::ColorFormat::ARGB, {}, r.begin(), r.end());
|
||||
auto format = algorithm == EScalingAlgorithm::XBRZ_OPAQUE ? xbrz::ColorFormat::ARGB_CLAMPED : xbrz::ColorFormat::ARGB;
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate, format](const tbb::blocked_range<size_t> & r)
|
||||
{
|
||||
|
||||
xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}, r.begin(), r.end());
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error("invalid scaling algorithm!");
|
||||
}
|
||||
|
@ -31,7 +31,8 @@ enum class EScalingAlgorithm : int8_t
|
||||
{
|
||||
NEAREST,
|
||||
BILINEAR,
|
||||
XBRZ
|
||||
XBRZ_OPAQUE, // xbrz, image edges are considered to have same color as pixel inside image
|
||||
XBRZ_ALPHA // xbrz, image edges are considered to be transparent
|
||||
};
|
||||
|
||||
namespace CSDL_Ext
|
||||
|
@ -173,7 +173,7 @@ void VideoWidgetBase::tick(uint32_t msPassed)
|
||||
{
|
||||
videoInstance->tick(msPassed);
|
||||
|
||||
if(subTitle)
|
||||
if(!videoInstance->videoEnded() && subTitle)
|
||||
subTitle->setText(getSubTitleLine(videoInstance->timeStamp()));
|
||||
|
||||
if(videoInstance->videoEnded())
|
||||
|
@ -198,8 +198,3 @@ void SettingsMainWindow::onScreenResize()
|
||||
if (tab)
|
||||
tab->updateResolutionSelector();
|
||||
}
|
||||
|
||||
void SettingsMainWindow::inputModeChanged(InputMode mode)
|
||||
{
|
||||
tabContentArea->reset();
|
||||
}
|
||||
|
@ -42,6 +42,5 @@ public:
|
||||
|
||||
void showAll(Canvas & to) override;
|
||||
void onScreenResize() override;
|
||||
void inputModeChanged(InputMode mode) override;
|
||||
};
|
||||
|
||||
|
@ -1195,6 +1195,22 @@ void xbrz::scale(size_t factor, const uint32_t* src, uint32_t* trg, int srcWidth
|
||||
}
|
||||
break;
|
||||
|
||||
case ColorFormat::ARGB_CLAMPED:
|
||||
switch (factor)
|
||||
{
|
||||
case 2:
|
||||
return scaleImage<Scaler2x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
|
||||
case 3:
|
||||
return scaleImage<Scaler3x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
|
||||
case 4:
|
||||
return scaleImage<Scaler4x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
|
||||
case 5:
|
||||
return scaleImage<Scaler5x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
|
||||
case 6:
|
||||
return scaleImage<Scaler6x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
|
||||
}
|
||||
break;
|
||||
|
||||
case ColorFormat::ARGB:
|
||||
switch (factor)
|
||||
{
|
||||
@ -1238,6 +1254,7 @@ bool xbrz::equalColorTest(uint32_t col1, uint32_t col2, ColorFormat colFmt, doub
|
||||
case ColorFormat::RGB:
|
||||
return ColorDistanceRGB::dist(col1, col2, luminanceWeight) < equalColorTolerance;
|
||||
case ColorFormat::ARGB:
|
||||
case ColorFormat::ARGB_CLAMPED:
|
||||
return ColorDistanceARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance;
|
||||
case ColorFormat::ARGB_UNBUFFERED:
|
||||
return ColorDistanceUnbufferedARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance;
|
||||
|
@ -44,6 +44,7 @@ enum class ColorFormat //from high bits -> low bits, 8 bit per channel
|
||||
{
|
||||
RGB, //8 bit for each red, green, blue, upper 8 bits unused
|
||||
ARGB, //including alpha channel, BGRA byte order on little-endian machines
|
||||
ARGB_CLAMPED, // like ARGB, but edges are treated as opaque, with same color as edge
|
||||
ARGB_UNBUFFERED, //like ARGB, but without the one-time buffer creation overhead (ca. 100 - 300 ms) at the expense of a slightly slower scaling time
|
||||
};
|
||||
|
||||
|
2
debian/changelog
vendored
2
debian/changelog
vendored
@ -2,7 +2,7 @@ vcmi (1.6.0) jammy; urgency=medium
|
||||
|
||||
* New upstream release
|
||||
|
||||
-- Ivan Savenko <saven.ivan@gmail.com> Fri, 30 Aug 2024 12:00:00 +0200
|
||||
-- Ivan Savenko <saven.ivan@gmail.com> Fri, 20 Dec 2024 12:00:00 +0200
|
||||
|
||||
vcmi (1.5.7) jammy; urgency=medium
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# VCMI Project
|
||||
|
||||
[](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
|
||||
[](https://github.com/vcmi/vcmi/releases/tag/1.5.0)
|
||||
[](https://github.com/vcmi/vcmi/releases/tag/1.5.6)
|
||||
[](https://github.com/vcmi/vcmi/releases/tag/1.5.7)
|
||||
[](https://github.com/vcmi/vcmi/releases/tag/1.6.0)
|
||||
[](https://github.com/vcmi/vcmi/releases)
|
||||
|
||||
VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.
|
||||
|
@ -15,6 +15,7 @@ set(launcher_SRCS
|
||||
modManager/imageviewer_moc.cpp
|
||||
modManager/chroniclesextractor.cpp
|
||||
settingsView/csettingsview_moc.cpp
|
||||
startGame/StartGameTab.cpp
|
||||
firstLaunch/firstlaunch_moc.cpp
|
||||
main.cpp
|
||||
helper.cpp
|
||||
@ -46,6 +47,7 @@ set(launcher_HEADERS
|
||||
modManager/imageviewer_moc.h
|
||||
modManager/chroniclesextractor.h
|
||||
settingsView/csettingsview_moc.h
|
||||
startGame/StartGameTab.h
|
||||
firstLaunch/firstlaunch_moc.h
|
||||
mainwindow_moc.h
|
||||
languages.h
|
||||
@ -63,6 +65,7 @@ set(launcher_FORMS
|
||||
settingsView/csettingsview_moc.ui
|
||||
firstLaunch/firstlaunch_moc.ui
|
||||
mainwindow_moc.ui
|
||||
startGame/StartGameTab.ui
|
||||
updatedialog_moc.ui
|
||||
)
|
||||
|
||||
|
@ -90,7 +90,7 @@
|
||||
</screenshots>
|
||||
<launchable type="desktop-id">vcmilauncher.desktop</launchable>
|
||||
<releases>
|
||||
<release version="1.6.0" date="2024-08-30" type="development"/>
|
||||
<release version="1.6.0" date="2024-12-20" type="stable"/>
|
||||
<release version="1.5.7" date="2024-08-26" type="stable"/>
|
||||
<release version="1.5.6" date="2024-08-04" type="stable"/>
|
||||
<release version="1.5.5" date="2024-07-17" type="stable"/>
|
||||
|
@ -295,13 +295,21 @@ bool FirstLaunchView::heroesDataDetect()
|
||||
QString FirstLaunchView::getHeroesInstallDir()
|
||||
{
|
||||
#ifdef VCMI_WINDOWS
|
||||
QString gogPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\GOG.com\\Games\\1207658787", QSettings::NativeFormat).value("path").toString();
|
||||
if(!gogPath.isEmpty())
|
||||
return gogPath;
|
||||
QVector<QPair<QString, QString>> regKeys = {
|
||||
{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\GOG.com\\Games\\1207658787", "path" }, // Gog on x86 system
|
||||
{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\GOG.com\\Games\\1207658787", "path" }, // Gog on x64 system
|
||||
{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic® III\\1.0", "AppPath" }, // H3 Complete on x86 system
|
||||
{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\New World Computing\\Heroes of Might and Magic® III\\1.0", "AppPath" }, // H3 Complete on x64 system
|
||||
{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic III\\1.0", "AppPath" }, // some localized H3 on x86 system
|
||||
{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\New World Computing\\Heroes of Might and Magic III\\1.0", "AppPath" }, // some localized H3 on x64 system
|
||||
};
|
||||
|
||||
QString cdPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic® III\\1.0", QSettings::NativeFormat).value("AppPath").toString();
|
||||
if(!cdPath.isEmpty())
|
||||
return cdPath;
|
||||
for(auto & regKey : regKeys)
|
||||
{
|
||||
QString path = QSettings(regKey.first, QSettings::NativeFormat).value(regKey.second).toString();
|
||||
if(!path.isEmpty())
|
||||
return path;
|
||||
}
|
||||
#endif
|
||||
return QString{};
|
||||
}
|
||||
@ -363,6 +371,9 @@ void FirstLaunchView::extractGogData()
|
||||
QFile(fileExe).copy(tmpFileExe);
|
||||
QFile(fileBin).copy(tmpFileBin);
|
||||
|
||||
logGlobal->info("Installing exe '%s' ('%s')", tmpFileExe.toStdString(), fileExe.toStdString());
|
||||
logGlobal->info("Installing bin '%s' ('%s')", tmpFileBin.toStdString(), fileBin.toStdString());
|
||||
|
||||
QString errorText{};
|
||||
|
||||
auto isGogGalaxyExe = [](QString fileToTest) {
|
||||
@ -381,7 +392,7 @@ void FirstLaunchView::extractGogData()
|
||||
};
|
||||
|
||||
if(isGogGalaxyExe(tmpFileExe))
|
||||
errorText = tr("You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer!");
|
||||
errorText = tr("You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer!");
|
||||
|
||||
if(errorText.isEmpty())
|
||||
errorText = Innoextract::extract(tmpFileExe, tempDir.path(), [this](float progress) {
|
||||
@ -451,7 +462,7 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move)
|
||||
QStringList dirMaps = sourceRoot.entryList({"maps"}, QDir::Filter::Dirs);
|
||||
QStringList dirMp3 = sourceRoot.entryList({"mp3"}, QDir::Filter::Dirs);
|
||||
|
||||
const auto noDataMessage = tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select directory with installed Heroes III data.");
|
||||
const auto noDataMessage = tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select the directory with installed Heroes III data.");
|
||||
if(dirData.empty())
|
||||
{
|
||||
QMessageBox::critical(this, tr("Heroes III data not found!"), noDataMessage);
|
||||
@ -475,12 +486,12 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move)
|
||||
if (!hdFiles.empty())
|
||||
{
|
||||
// HD Edition contains only RoE data so we can't use even unmodified files from it
|
||||
QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Heroes III: HD Edition files are not supported by VCMI.\nPlease select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death."));
|
||||
QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Heroes III: HD Edition files are not supported by VCMI.\nPlease select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death."));
|
||||
return;
|
||||
}
|
||||
|
||||
// RoE or some other unsupported edition. Demo version?
|
||||
QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Unknown or unsupported Heroes III version found.\nPlease select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death."));
|
||||
QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Unknown or unsupported Heroes III version found.\nPlease select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death."));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -541,15 +552,13 @@ void FirstLaunchView::modPresetUpdate()
|
||||
|
||||
QString FirstLaunchView::findTranslationModName()
|
||||
{
|
||||
if (!getModView())
|
||||
auto * mainWindow = dynamic_cast<MainWindow *>(QApplication::activeWindow());
|
||||
auto status = mainWindow->getTranslationStatus();
|
||||
|
||||
if (status == ETranslationStatus::ACTIVE || status == ETranslationStatus::NOT_AVAILABLE)
|
||||
return QString();
|
||||
|
||||
QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String());
|
||||
QString installedlanguage = QString::fromStdString(settings["session"]["language"].String());
|
||||
|
||||
if (preferredlanguage == installedlanguage)
|
||||
return QString();
|
||||
|
||||
return getModView()->getTranslationModName(preferredlanguage);
|
||||
}
|
||||
|
||||
|
@ -55,8 +55,6 @@ class FirstLaunchView : public QWidget
|
||||
bool checkCanInstallExtras();
|
||||
bool checkCanInstallMod(const QString & modID);
|
||||
|
||||
void installMod(const QString & modID);
|
||||
|
||||
public:
|
||||
explicit FirstLaunchView(QWidget * parent = nullptr);
|
||||
|
||||
|
@ -96,7 +96,7 @@
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="installerTabs">
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
<number>2</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="pageLanguageSelect">
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
@ -177,9 +177,9 @@
|
||||
<property name="text">
|
||||
<string>Thank you for installing VCMI!
|
||||
|
||||
Before you can start playing, there are a few more steps that need to be completed.
|
||||
Before you can start playing, there are a few more steps to complete.
|
||||
|
||||
Please keep in mind that in order to use VCMI you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death.
|
||||
Please remember that to use VCMI, you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death.
|
||||
|
||||
Heroes® of Might and Magic® III HD is currently not supported!</string>
|
||||
</property>
|
||||
@ -307,7 +307,7 @@ Heroes® of Might and Magic® III HD is currently not supported!</string>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>You can manually copy directories Maps, Data and Mp3 from the original game directory to VCMI data directory that you can see on top of this page</string>
|
||||
<string>You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
@ -501,8 +501,8 @@ Heroes® of Might and Magic® III HD is currently not supported!</string>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>If you own Heroes III on gog.com you can download backup offline installer from gog.com, and VCMI will import Heroes III data using offline installer.
|
||||
Offline installer consists of two parts, .exe and .bin. Make sure you download both of them.</string>
|
||||
<string>If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer.
|
||||
Offline installer consists of two files: ".exe" and ".bin" - you must download both.</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
@ -785,7 +785,7 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</string>
|
||||
<string>Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
@ -809,9 +809,9 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b
|
||||
<string>In The Wake of Gods</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<iconset resource="../resources.qrc">
|
||||
<normaloff>:/icons/mod-disabled.png</normaloff>
|
||||
<normalon>:icons/mod-enabled.png</normalon>:icons/mod-disabled.png</iconset>
|
||||
<normalon>:icons/mod-enabled.png</normalon>:/icons/mod-disabled.png</iconset>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
@ -876,6 +876,8 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<resources>
|
||||
<include location="../resources.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -83,6 +83,8 @@ QString Innoextract::getHashError(QString exeFile, QString binFile, QString exeF
|
||||
};
|
||||
|
||||
std::vector<data> knownHashes = {
|
||||
// { H3_COMPLETE, "english", 973162040, 0, "7cf1ecec73e8c2f2c2619415cd16749be5641942", "" }, // setup_homm_3_complete_4.0_(10665).exe
|
||||
// { H3_COMPLETE, "french", ???, 0, "7e5a737c51530a1888033d188ab0635825ee622f", "" }, // setup_homm_3_complete_french_4.0_(10665).exe
|
||||
{ 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
|
||||
|
@ -47,7 +47,6 @@ void MainWindow::computeSidePanelSizes()
|
||||
ui->modslistButton,
|
||||
ui->settingsButton,
|
||||
ui->aboutButton,
|
||||
ui->startEditorButton,
|
||||
ui->startGameButton
|
||||
};
|
||||
|
||||
@ -78,11 +77,12 @@ MainWindow::MainWindow(QWidget * parent)
|
||||
|
||||
ui->setupUi(this);
|
||||
|
||||
setAcceptDrops(true);
|
||||
|
||||
setWindowIcon(QIcon{":/icons/menu-game.png"});
|
||||
ui->modslistButton->setIcon(QIcon{":/icons/menu-mods.png"});
|
||||
ui->settingsButton->setIcon(QIcon{":/icons/menu-settings.png"});
|
||||
ui->aboutButton->setIcon(QIcon{":/icons/about-project.png"});
|
||||
ui->startEditorButton->setIcon(QIcon{":/icons/menu-editor.png"});
|
||||
ui->startGameButton->setIcon(QIcon{":/icons/menu-game.png"});
|
||||
|
||||
#ifndef VCMI_MOBILE
|
||||
@ -101,16 +101,12 @@ MainWindow::MainWindow(QWidget * parent)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef ENABLE_EDITOR
|
||||
ui->startEditorButton->hide();
|
||||
#endif
|
||||
|
||||
computeSidePanelSizes();
|
||||
|
||||
bool h3DataFound = CResourceHandler::get()->existsResource(ResourcePath("DATA/GENRLTXT.TXT"));
|
||||
|
||||
if (h3DataFound && setupCompleted)
|
||||
ui->tabListWidget->setCurrentIndex(TabRows::MODS);
|
||||
ui->tabListWidget->setCurrentIndex(TabRows::START);
|
||||
else
|
||||
enterSetup();
|
||||
|
||||
@ -147,7 +143,6 @@ void MainWindow::detectPreferredLanguage()
|
||||
void MainWindow::enterSetup()
|
||||
{
|
||||
ui->startGameButton->setEnabled(false);
|
||||
ui->startEditorButton->setEnabled(false);
|
||||
ui->settingsButton->setEnabled(false);
|
||||
ui->aboutButton->setEnabled(false);
|
||||
ui->modslistButton->setEnabled(false);
|
||||
@ -160,16 +155,27 @@ void MainWindow::exitSetup()
|
||||
writer->Bool() = true;
|
||||
|
||||
ui->startGameButton->setEnabled(true);
|
||||
ui->startEditorButton->setEnabled(true);
|
||||
ui->settingsButton->setEnabled(true);
|
||||
ui->aboutButton->setEnabled(true);
|
||||
ui->modslistButton->setEnabled(true);
|
||||
ui->tabListWidget->setCurrentIndex(TabRows::MODS);
|
||||
}
|
||||
|
||||
void MainWindow::switchToStartTab()
|
||||
{
|
||||
ui->startGameButton->setEnabled(true);
|
||||
ui->startGameButton->setChecked(true);
|
||||
ui->tabListWidget->setCurrentIndex(TabRows::START);
|
||||
|
||||
auto* startGameTabWidget = qobject_cast<StartGameTab*>(ui->tabListWidget->widget(TabRows::START));
|
||||
if(startGameTabWidget)
|
||||
startGameTabWidget->refreshState();
|
||||
}
|
||||
|
||||
void MainWindow::switchToModsTab()
|
||||
{
|
||||
ui->startGameButton->setEnabled(true);
|
||||
ui->modslistButton->setChecked(true);
|
||||
ui->tabListWidget->setCurrentIndex(TabRows::MODS);
|
||||
}
|
||||
|
||||
@ -196,14 +202,7 @@ MainWindow::~MainWindow()
|
||||
|
||||
void MainWindow::on_startGameButton_clicked()
|
||||
{
|
||||
hide();
|
||||
startGame({});
|
||||
}
|
||||
|
||||
void MainWindow::on_startEditorButton_clicked()
|
||||
{
|
||||
hide();
|
||||
startEditor({});
|
||||
switchToStartTab();
|
||||
}
|
||||
|
||||
CModListView * MainWindow::getModView()
|
||||
@ -228,6 +227,95 @@ void MainWindow::on_aboutButton_clicked()
|
||||
ui->tabListWidget->setCurrentIndex(TabRows::ABOUT);
|
||||
}
|
||||
|
||||
void MainWindow::dragEnterEvent(QDragEnterEvent* event)
|
||||
{
|
||||
if(event->mimeData()->hasUrls())
|
||||
for(const auto & url : event->mimeData()->urls())
|
||||
for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json", ".exe"}))
|
||||
if(url.fileName().endsWith(ending, Qt::CaseInsensitive))
|
||||
{
|
||||
event->acceptProposedAction();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::dropEvent(QDropEvent* event)
|
||||
{
|
||||
const QMimeData* mimeData = event->mimeData();
|
||||
|
||||
if(mimeData->hasUrls())
|
||||
{
|
||||
const QList<QUrl> urlList = mimeData->urls();
|
||||
for (const auto & url : urlList)
|
||||
manualInstallFile(url.toLocalFile());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::manualInstallFile(QString filePath)
|
||||
{
|
||||
if(filePath.endsWith(".zip", Qt::CaseInsensitive) || filePath.endsWith(".exe", Qt::CaseInsensitive))
|
||||
switchToModsTab();
|
||||
|
||||
QString fileName = QFileInfo{filePath}.fileName();
|
||||
if(filePath.endsWith(".zip", Qt::CaseInsensitive))
|
||||
{
|
||||
QString filenameClean = fileName.toLower()
|
||||
// mod name currently comes from zip file -> remove suffixes from github zip download
|
||||
.replace(QRegularExpression("-[0-9a-f]{40}"), "")
|
||||
.replace(QRegularExpression("-vcmi-.+\\.zip"), ".zip")
|
||||
.replace("-main.zip", ".zip");
|
||||
|
||||
getModView()->downloadFile(filenameClean, QUrl::fromLocalFile(filePath), "mods");
|
||||
}
|
||||
else if(filePath.endsWith(".json", Qt::CaseInsensitive))
|
||||
{
|
||||
QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string()));
|
||||
QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check
|
||||
if(!configFile.empty())
|
||||
{
|
||||
auto dialogResult = QMessageBox::warning(this, tr("Replace config file?"), tr("Do you want to replace %1?").arg(configFile[0]), QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if(dialogResult == QMessageBox::Yes)
|
||||
{
|
||||
const auto configFilePath = configDir.filePath(configFile[0]);
|
||||
QFile::remove(configFilePath);
|
||||
QFile::copy(filePath, configFilePath);
|
||||
|
||||
// reload settings
|
||||
Helper::loadSettings();
|
||||
for(const auto widget : qApp->allWidgets())
|
||||
if(auto settingsView = qobject_cast<CSettingsView *>(widget))
|
||||
settingsView->loadSettings();
|
||||
|
||||
getModView()->reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
getModView()->installFiles(QStringList{filePath});
|
||||
}
|
||||
|
||||
ETranslationStatus MainWindow::getTranslationStatus()
|
||||
{
|
||||
QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String());
|
||||
QString installedlanguage = QString::fromStdString(settings["session"]["language"].String());
|
||||
|
||||
if (preferredlanguage == installedlanguage)
|
||||
return ETranslationStatus::ACTIVE;
|
||||
|
||||
QString modName = getModView()->getTranslationModName(preferredlanguage);
|
||||
|
||||
if (modName.isEmpty())
|
||||
return ETranslationStatus::NOT_AVAILABLE;
|
||||
|
||||
if (!getModView()->isModInstalled(modName))
|
||||
return ETranslationStatus::NOT_INSTALLLED;
|
||||
|
||||
if (!getModView()->isModEnabled(modName))
|
||||
return ETranslationStatus::DISABLED;
|
||||
|
||||
return ETranslationStatus::ACTIVE;
|
||||
}
|
||||
|
||||
void MainWindow::updateTranslation()
|
||||
{
|
||||
#ifdef ENABLE_QT_TRANSLATIONS
|
||||
|
@ -23,6 +23,14 @@ class QTableWidgetItem;
|
||||
class CModList;
|
||||
class CModListView;
|
||||
|
||||
enum class ETranslationStatus : int8_t
|
||||
{
|
||||
NOT_AVAILABLE, // translation for this language was not found in mod list. Could also happen if player is offline or disabled repository checkout
|
||||
NOT_INSTALLLED, // translation mod found, but it is not installed
|
||||
DISABLED, // translation mod found, and installed, but toggled off
|
||||
ACTIVE // translation mod active OR game is already in specified language (e.g. English H3 for players with English language)
|
||||
};
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -40,6 +48,7 @@ class MainWindow : public QMainWindow
|
||||
SETTINGS = 1,
|
||||
SETUP = 2,
|
||||
ABOUT = 3,
|
||||
START = 4,
|
||||
};
|
||||
|
||||
public:
|
||||
@ -55,6 +64,13 @@ public:
|
||||
void enterSetup();
|
||||
void exitSetup();
|
||||
void switchToModsTab();
|
||||
void switchToStartTab();
|
||||
|
||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
|
||||
void manualInstallFile(QString filePath);
|
||||
ETranslationStatus getTranslationStatus();
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent * event) override;
|
||||
@ -65,6 +81,5 @@ public slots:
|
||||
private slots:
|
||||
void on_modslistButton_clicked();
|
||||
void on_settingsButton_clicked();
|
||||
void on_startEditorButton_clicked();
|
||||
void on_aboutButton_clicked();
|
||||
};
|
||||
|
@ -29,6 +29,57 @@
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QToolButton" name="startGameButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Game</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="modslistButton">
|
||||
<property name="sizePolicy">
|
||||
@ -62,7 +113,7 @@
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>true</bool>
|
||||
@ -146,8 +197,8 @@
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
<width>48</width>
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
@ -180,104 +231,6 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="startEditorButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>5</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Map Editor</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="startGameButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Start game</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
@ -292,12 +245,13 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>3</number>
|
||||
<number>4</number>
|
||||
</property>
|
||||
<widget class="CModListView" name="modlistView"/>
|
||||
<widget class="CSettingsView" name="settingsView"/>
|
||||
<widget class="FirstLaunchView" name="setupView"/>
|
||||
<widget class="AboutProjectView" name="aboutView"/>
|
||||
<widget class="StartGameTab" name="startGameView"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
@ -329,6 +283,12 @@
|
||||
<header>aboutProject/aboutproject_moc.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>StartGameTab</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>startGame/StartGameTab.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
|
@ -41,38 +41,26 @@ void ChroniclesExtractor::removeTempDir()
|
||||
tempDir.removeRecursively();
|
||||
}
|
||||
|
||||
int ChroniclesExtractor::getChronicleNo(QFile & file)
|
||||
int ChroniclesExtractor::getChronicleNo()
|
||||
{
|
||||
if(!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
QMessageBox::critical(parent, tr("File cannot opened"), file.errorString());
|
||||
return 0;
|
||||
}
|
||||
QStringList appDirCandidates = tempDir.entryList({"app"}, QDir::Filter::Dirs);
|
||||
|
||||
QByteArray magic{"MZ"};
|
||||
QByteArray magicFile = file.read(magic.length());
|
||||
if(!magicFile.startsWith(magic))
|
||||
if (!appDirCandidates.empty())
|
||||
{
|
||||
QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select an gog installer file!"));
|
||||
return 0;
|
||||
}
|
||||
QDir appDir = tempDir.filePath(appDirCandidates.front());
|
||||
|
||||
QByteArray dataBegin = file.read(1'000'000);
|
||||
int chronicle = 0;
|
||||
for (const auto& kv : chronicles) {
|
||||
if(dataBegin.contains(kv.second))
|
||||
for (size_t i = 1; i < chronicles.size(); ++i)
|
||||
{
|
||||
chronicle = kv.first;
|
||||
break;
|
||||
QString chronicleName = chronicles.at(i);
|
||||
QStringList chroniclesDirCandidates = appDir.entryList({chronicleName}, QDir::Filter::Dirs);
|
||||
|
||||
if (!chroniclesDirCandidates.empty())
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if(!chronicle)
|
||||
{
|
||||
QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select an chronicle installer file!"));
|
||||
QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select a Heroes Chronicles installer file!"));
|
||||
return 0;
|
||||
}
|
||||
return chronicle;
|
||||
}
|
||||
|
||||
bool ChroniclesExtractor::extractGogInstaller(QString file)
|
||||
{
|
||||
@ -129,16 +117,14 @@ void ChroniclesExtractor::createBaseMod() const
|
||||
|
||||
for(auto & dataPath : VCMIDirs::get().dataPaths())
|
||||
{
|
||||
auto file = dataPath / "config" / "heroes" / "portraitsChronicles.json";
|
||||
auto file = pathToQString(dataPath / "config" / "heroes" / "portraitsChronicles.json");
|
||||
auto destFolder = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "config";
|
||||
if(boost::filesystem::exists(file))
|
||||
auto destFile = pathToQString(destFolder / "portraitsChronicles.json");
|
||||
if(QFile::exists(file))
|
||||
{
|
||||
boost::filesystem::create_directories(destFolder);
|
||||
#if BOOST_VERSION >= 107400
|
||||
boost::filesystem::copy_file(file, destFolder / "portraitsChronicles.json", boost::filesystem::copy_options::overwrite_existing);
|
||||
#else
|
||||
boost::filesystem::copy_file(file, destFolder / "portraitsChronicles.json", boost::filesystem::copy_option::overwrite_if_exists);
|
||||
#endif
|
||||
QDir().mkpath(pathToQString(destFolder));
|
||||
QFile::remove(destFile);
|
||||
QFile::copy(file, destFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,14 +135,13 @@ void ChroniclesExtractor::createChronicleMod(int no)
|
||||
dir.removeRecursively();
|
||||
dir.mkpath(".");
|
||||
|
||||
QByteArray tmpChronicles = chronicles.at(no);
|
||||
tmpChronicles.replace('\0', "");
|
||||
QString tmpChronicles = chronicles.at(no);
|
||||
|
||||
QJsonObject mod
|
||||
{
|
||||
{ "modType", "Expansion" },
|
||||
{ "name", QString::number(no) + " - " + QString(tmpChronicles) },
|
||||
{ "description", tr("Heroes Chronicles") + " - " + QString::number(no) + " - " + QString(tmpChronicles) },
|
||||
{ "name", QString("%1 - %2").arg(no).arg(tmpChronicles) },
|
||||
{ "description", tr("Heroes Chronicles %1 - %2").arg(no).arg(tmpChronicles) },
|
||||
{ "author", "3DO" },
|
||||
{ "version", "1.0" },
|
||||
{ "contact", "vcmi.eu" },
|
||||
@ -173,8 +158,7 @@ void ChroniclesExtractor::createChronicleMod(int no)
|
||||
|
||||
void ChroniclesExtractor::extractFiles(int no) const
|
||||
{
|
||||
QByteArray tmpChronicles = chronicles.at(no);
|
||||
tmpChronicles.replace('\0', "");
|
||||
QString tmpChronicles = chronicles.at(no);
|
||||
|
||||
std::string chroniclesDir = "chronicles_" + std::to_string(no);
|
||||
QDir tmpDir = tempDir.filePath(tempDir.entryList({"app"}, QDir::Filter::Dirs).front());
|
||||
@ -230,26 +214,46 @@ void ChroniclesExtractor::extractFiles(int no) const
|
||||
|
||||
void ChroniclesExtractor::installChronicles(QStringList exe)
|
||||
{
|
||||
logGlobal->info("Installing Chronicles");
|
||||
|
||||
extractionFile = -1;
|
||||
fileCount = exe.size();
|
||||
for(QString f : exe)
|
||||
{
|
||||
extractionFile++;
|
||||
QFile file(f);
|
||||
|
||||
int chronicleNo = getChronicleNo(file);
|
||||
if(!chronicleNo)
|
||||
continue;
|
||||
|
||||
logGlobal->info("Creating temporary directory");
|
||||
if(!createTempDir())
|
||||
continue;
|
||||
|
||||
if(!extractGogInstaller(f))
|
||||
logGlobal->info("Copying offline installer");
|
||||
// FIXME: this is required at the moment for Android (and possibly iOS)
|
||||
// Incoming file names are in content URI form, e.g. content://media/internal/chronicles.exe
|
||||
// Qt can handle those like it does regular files
|
||||
// however, innoextract fails to open such files
|
||||
// so make a copy in directory to which vcmi always has full access and operate on it
|
||||
QString filepath = tempDir.filePath("chr.exe");
|
||||
QFile(f).copy(filepath);
|
||||
QFile file(filepath);
|
||||
|
||||
logGlobal->info("Extracting offline installer");
|
||||
if(!extractGogInstaller(filepath))
|
||||
continue;
|
||||
|
||||
logGlobal->info("Detecting Chronicle");
|
||||
int chronicleNo = getChronicleNo();
|
||||
if(!chronicleNo)
|
||||
continue;
|
||||
|
||||
logGlobal->info("Creating base Chronicle mod");
|
||||
createBaseMod();
|
||||
|
||||
logGlobal->info("Creating Chronicle mod");
|
||||
createChronicleMod(chronicleNo);
|
||||
|
||||
logGlobal->info("Removing temporary directory");
|
||||
removeTempDir();
|
||||
}
|
||||
|
||||
logGlobal->info("Chronicles installed");
|
||||
}
|
||||
|
@ -24,21 +24,22 @@ class ChroniclesExtractor : public QObject
|
||||
|
||||
bool createTempDir();
|
||||
void removeTempDir();
|
||||
int getChronicleNo(QFile & file);
|
||||
int getChronicleNo();
|
||||
bool extractGogInstaller(QString filePath);
|
||||
void createBaseMod() const;
|
||||
void createChronicleMod(int no);
|
||||
void extractFiles(int no) const;
|
||||
|
||||
const std::map<int, QByteArray> chronicles = {
|
||||
{1, QByteArray{reinterpret_cast<const char*>(u"Warlords of the Wasteland"), 50}},
|
||||
{2, QByteArray{reinterpret_cast<const char*>(u"Conquest of the Underworld"), 52}},
|
||||
{3, QByteArray{reinterpret_cast<const char*>(u"Masters of the Elements"), 46}},
|
||||
{4, QByteArray{reinterpret_cast<const char*>(u"Clash of the Dragons"), 40}},
|
||||
{5, QByteArray{reinterpret_cast<const char*>(u"The World Tree"), 28}},
|
||||
{6, QByteArray{reinterpret_cast<const char*>(u"The Fiery Moon"), 28}},
|
||||
{7, QByteArray{reinterpret_cast<const char*>(u"Revolt of the Beastmasters"), 52}},
|
||||
{8, QByteArray{reinterpret_cast<const char*>(u"The Sword of Frost"), 36}}
|
||||
const QStringList chronicles = {
|
||||
{}, // fake 0th "chronicle", to create 1-based list
|
||||
"Warlords of the Wasteland",
|
||||
"Conquest of the Underworld",
|
||||
"Masters of the Elements",
|
||||
"Clash of the Dragons",
|
||||
"The World Tree",
|
||||
"The Fiery Moon",
|
||||
"Revolt of the Beastmasters",
|
||||
"The Sword of Frost",
|
||||
};
|
||||
public:
|
||||
void installChronicles(QStringList exe);
|
||||
|
@ -27,12 +27,13 @@
|
||||
#include "../vcmiqt/jsonutils.h"
|
||||
#include "../helper.h"
|
||||
|
||||
#include "../../lib/VCMIDirs.h"
|
||||
#include "../../lib/CConfigHandler.h"
|
||||
#include "../../lib/texts/Languages.h"
|
||||
#include "../../lib/modding/CModVersion.h"
|
||||
#include "../../lib/VCMIDirs.h"
|
||||
#include "../../lib/filesystem/Filesystem.h"
|
||||
#include "../../lib/json/JsonUtils.h"
|
||||
#include "../../lib/modding/CModVersion.h"
|
||||
#include "../../lib/texts/CGeneralTextHandler.h"
|
||||
#include "../../lib/texts/Languages.h"
|
||||
|
||||
#include <future>
|
||||
|
||||
@ -43,7 +44,7 @@ void CModListView::setupModModel()
|
||||
|
||||
modStateModel = std::make_shared<ModStateModel>();
|
||||
if (!cachedRepositoryData.isNull())
|
||||
modStateModel->appendRepositories(cachedRepositoryData);
|
||||
modStateModel->setRepositoryData(cachedRepositoryData);
|
||||
|
||||
modModel = new ModStateItemModel(modStateModel, this);
|
||||
manager = std::make_unique<ModStateController>(modStateModel);
|
||||
@ -54,35 +55,11 @@ void CModListView::changeEvent(QEvent *event)
|
||||
if(event->type() == QEvent::LanguageChange)
|
||||
{
|
||||
ui->retranslateUi(this);
|
||||
modModel->reloadRepositories();
|
||||
modModel->reloadViewModel();
|
||||
}
|
||||
QWidget::changeEvent(event);
|
||||
}
|
||||
|
||||
void CModListView::dragEnterEvent(QDragEnterEvent* event)
|
||||
{
|
||||
if(event->mimeData()->hasUrls())
|
||||
for(const auto & url : event->mimeData()->urls())
|
||||
for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json", ".exe"}))
|
||||
if(url.fileName().endsWith(ending, Qt::CaseInsensitive))
|
||||
{
|
||||
event->acceptProposedAction();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CModListView::dropEvent(QDropEvent* event)
|
||||
{
|
||||
const QMimeData* mimeData = event->mimeData();
|
||||
|
||||
if(mimeData->hasUrls())
|
||||
{
|
||||
const QList<QUrl> urlList = mimeData->urls();
|
||||
for (const auto & url : urlList)
|
||||
manualInstallFile(url.toLocalFile());
|
||||
}
|
||||
}
|
||||
|
||||
void CModListView::setupFilterModel()
|
||||
{
|
||||
filterModel = new CModFilterModel(modModel, this);
|
||||
@ -134,8 +111,6 @@ CModListView::CModListView(QWidget * parent)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
setAcceptDrops(true);
|
||||
|
||||
ui->uninstallButton->setIcon(QIcon{":/icons/mod-delete.png"});
|
||||
ui->enableButton->setIcon(QIcon{":/icons/mod-enabled.png"});
|
||||
ui->disableButton->setIcon(QIcon{":/icons/mod-disabled.png"});
|
||||
@ -152,7 +127,7 @@ CModListView::CModListView(QWidget * parent)
|
||||
ui->progressWidget->setVisible(false);
|
||||
dlManager = nullptr;
|
||||
|
||||
modModel->reloadRepositories();
|
||||
modModel->reloadViewModel();
|
||||
if(settings["launcher"]["autoCheckRepositories"].Bool())
|
||||
loadRepositories();
|
||||
|
||||
@ -169,8 +144,16 @@ CModListView::CModListView(QWidget * parent)
|
||||
#endif
|
||||
}
|
||||
|
||||
void CModListView::reload()
|
||||
{
|
||||
modStateModel->reloadLocalState();
|
||||
modModel->reloadViewModel();
|
||||
}
|
||||
|
||||
void CModListView::loadRepositories()
|
||||
{
|
||||
accumulatedRepositoryData.clear();
|
||||
|
||||
QStringList repositories;
|
||||
|
||||
if (settings["launcher"]["defaultRepositoryEnabled"].Bool())
|
||||
@ -461,9 +444,10 @@ void CModListView::selectMod(const QModelIndex & index)
|
||||
Helper::enableScrollBySwiping(ui->modInfoBrowser);
|
||||
Helper::enableScrollBySwiping(ui->changelogBrowser);
|
||||
|
||||
QStringList notInstalledDependencies = this->getModsToInstall(modName);
|
||||
QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies);
|
||||
QStringList notInstalledDependencies = getModsToInstall(modName);
|
||||
QStringList unavailableDependencies = findUnavailableMods(notInstalledDependencies);
|
||||
bool translationMismatch = mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString();
|
||||
bool modIsBeingDownloaded = enqueuedModDownloads.contains(mod.getID());
|
||||
|
||||
ui->disableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && modStateModel->isModEnabled(mod.getID()));
|
||||
ui->enableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && !modStateModel->isModEnabled(mod.getID()));
|
||||
@ -474,9 +458,9 @@ void CModListView::selectMod(const QModelIndex & index)
|
||||
// Block buttons if action is not allowed at this time
|
||||
ui->disableButton->setEnabled(true);
|
||||
ui->enableButton->setEnabled(notInstalledDependencies.empty() && !translationMismatch);
|
||||
ui->installButton->setEnabled(unavailableDependencies.empty());
|
||||
ui->installButton->setEnabled(unavailableDependencies.empty() && !modIsBeingDownloaded);
|
||||
ui->uninstallButton->setEnabled(true);
|
||||
ui->updateButton->setEnabled(unavailableDependencies.empty());
|
||||
ui->updateButton->setEnabled(unavailableDependencies.empty() && !modIsBeingDownloaded);
|
||||
|
||||
loadScreenshots();
|
||||
}
|
||||
@ -564,9 +548,6 @@ QStringList CModListView::getModsToInstall(QString mod)
|
||||
candidates.pop_back();
|
||||
processed.push_back(potentialToInstall);
|
||||
|
||||
if (modStateModel->isModExists(potentialToInstall) && modStateModel->isModInstalled(potentialToInstall))
|
||||
continue;
|
||||
|
||||
if (modStateModel->isSubmod(potentialToInstall))
|
||||
{
|
||||
QString topParent = modStateModel->getTopParent(potentialToInstall);
|
||||
@ -580,6 +561,7 @@ QStringList CModListView::getModsToInstall(QString mod)
|
||||
potentialToInstall = modStateModel->getTopParent(potentialToInstall);
|
||||
}
|
||||
|
||||
if (modStateModel->isModExists(potentialToInstall) && !modStateModel->isModInstalled(potentialToInstall))
|
||||
result.push_back(potentialToInstall);
|
||||
|
||||
if (modStateModel->isModExists(potentialToInstall))
|
||||
@ -599,17 +581,24 @@ QStringList CModListView::getModsToInstall(QString mod)
|
||||
void CModListView::on_updateButton_clicked()
|
||||
{
|
||||
QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
|
||||
doUpdateMod(modName);
|
||||
|
||||
ui->updateButton->setEnabled(false);
|
||||
}
|
||||
|
||||
void CModListView::doUpdateMod(const QString & modName)
|
||||
{
|
||||
auto targetMod = modStateModel->getMod(modName);
|
||||
|
||||
if(targetMod.isUpdateAvailable())
|
||||
downloadFile(modName + ".zip", targetMod.getDownloadUrl(), modName, targetMod.getDownloadSizeBytes());
|
||||
downloadMod(targetMod);
|
||||
|
||||
for(const auto & name : getModsToInstall(modName))
|
||||
{
|
||||
auto mod = modStateModel->getMod(name);
|
||||
// update required mod, install missing (can be new dependency)
|
||||
if(mod.isUpdateAvailable() || !mod.isInstalled())
|
||||
downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes());
|
||||
downloadMod(mod);
|
||||
}
|
||||
}
|
||||
|
||||
@ -622,7 +611,7 @@ void CModListView::on_uninstallButton_clicked()
|
||||
if(modStateModel->isModEnabled(modName))
|
||||
manager->disableMod(modName);
|
||||
manager->uninstallMod(modName);
|
||||
modModel->reloadRepositories();
|
||||
reload();
|
||||
}
|
||||
|
||||
checkManagerErrors();
|
||||
@ -632,81 +621,18 @@ void CModListView::on_installButton_clicked()
|
||||
{
|
||||
QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
|
||||
|
||||
for(const auto & name : getModsToInstall(modName))
|
||||
{
|
||||
auto mod = modStateModel->getMod(name);
|
||||
if(mod.isAvailable())
|
||||
downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes());
|
||||
else if(!modStateModel->isModEnabled(name))
|
||||
enableModByName(name);
|
||||
}
|
||||
doInstallMod(modName);
|
||||
|
||||
ui->installButton->setEnabled(false);
|
||||
}
|
||||
|
||||
void CModListView::on_installFromFileButton_clicked()
|
||||
void CModListView::downloadMod(const ModState & mod)
|
||||
{
|
||||
// iOS can't display modal dialogs when called directly on button press
|
||||
// https://bugreports.qt.io/browse/QTBUG-98651
|
||||
QTimer::singleShot(0, this, [this]
|
||||
{
|
||||
QString filter = tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json *.exe);;" +
|
||||
tr("Maps") + " (*.h3m *.vmap);;" +
|
||||
tr("Campaigns") + " (*.h3c *.vcmp);;" +
|
||||
tr("Configs") + " (*.json);;" +
|
||||
tr("Mods") + " (*.zip);;" +
|
||||
tr("Gog files") + " (*.exe)";
|
||||
#if defined(VCMI_MOBILE)
|
||||
filter = tr("All files (*.*)"); //Workaround for sometimes incorrect mime for some extensions (e.g. for exe)
|
||||
#endif
|
||||
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns, gog files) to install..."), QDir::homePath(), filter);
|
||||
if (enqueuedModDownloads.contains(mod.getID()))
|
||||
return;
|
||||
|
||||
for(const auto & file : files)
|
||||
{
|
||||
manualInstallFile(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CModListView::manualInstallFile(QString filePath)
|
||||
{
|
||||
QString fileName = QFileInfo{filePath}.fileName();
|
||||
if(filePath.endsWith(".zip", Qt::CaseInsensitive))
|
||||
downloadFile(fileName.toLower()
|
||||
// mod name currently comes from zip file -> remove suffixes from github zip download
|
||||
.replace(QRegularExpression("-[0-9a-f]{40}"), "")
|
||||
.replace(QRegularExpression("-vcmi-.+\\.zip"), ".zip")
|
||||
.replace("-main.zip", ".zip")
|
||||
, QUrl::fromLocalFile(filePath), "mods");
|
||||
else if(filePath.endsWith(".json", Qt::CaseInsensitive))
|
||||
{
|
||||
QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string()));
|
||||
QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check
|
||||
if(!configFile.empty())
|
||||
{
|
||||
auto dialogResult = QMessageBox::warning(this, tr("Replace config file?"), tr("Do you want to replace %1?").arg(configFile[0]), QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if(dialogResult == QMessageBox::Yes)
|
||||
{
|
||||
const auto configFilePath = configDir.filePath(configFile[0]);
|
||||
QFile::remove(configFilePath);
|
||||
QFile::copy(filePath, configFilePath);
|
||||
|
||||
// reload settings
|
||||
Helper::loadSettings();
|
||||
for(const auto widget : qApp->allWidgets())
|
||||
if(auto settingsView = qobject_cast<CSettingsView *>(widget))
|
||||
settingsView->loadSettings();
|
||||
|
||||
modStateModel->reloadLocalState();
|
||||
modModel->reloadRepositories();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
downloadFile(fileName, QUrl::fromLocalFile(filePath), fileName);
|
||||
}
|
||||
|
||||
void CModListView::downloadFile(QString file, QString url, QString description, qint64 sizeBytes)
|
||||
{
|
||||
downloadFile(file, QUrl{url}, description, sizeBytes);
|
||||
enqueuedModDownloads.push_back(mod.getID());
|
||||
downloadFile(mod.getID() + ".zip", mod.getDownloadUrl(), mod.getName(), mod.getDownloadSizeBytes());
|
||||
}
|
||||
|
||||
void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes)
|
||||
@ -779,6 +705,7 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi
|
||||
doInstallFiles = true;
|
||||
}
|
||||
|
||||
enqueuedModDownloads.clear();
|
||||
dlManager->deleteLater();
|
||||
dlManager = nullptr;
|
||||
|
||||
@ -807,7 +734,7 @@ void CModListView::installFiles(QStringList files)
|
||||
QStringList maps;
|
||||
QStringList images;
|
||||
QStringList exe;
|
||||
JsonNode repository;
|
||||
bool repositoryFilesEnqueued = false;
|
||||
|
||||
// TODO: some better way to separate zip's with mods and downloaded repository files
|
||||
for(QString filename : files)
|
||||
@ -821,7 +748,7 @@ void CModListView::installFiles(QStringList files)
|
||||
else if(filename.endsWith(".json", Qt::CaseInsensitive))
|
||||
{
|
||||
//download and merge additional files
|
||||
const auto &repoData = JsonUtils::jsonFromFile(filename);
|
||||
JsonNode repoData = JsonUtils::jsonFromFile(filename);
|
||||
if(repoData["name"].isNull())
|
||||
{
|
||||
// This is main repository index. Download all referenced mods
|
||||
@ -830,9 +757,12 @@ void CModListView::installFiles(QStringList files)
|
||||
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"));
|
||||
repositoryFilesEnqueued = true;
|
||||
}
|
||||
|
||||
repository[modNameLower] = modJson;
|
||||
accumulatedRepositoryData[modNameLower] = modJson;
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -840,35 +770,46 @@ void CModListView::installFiles(QStringList files)
|
||||
// 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;
|
||||
JsonUtils::merge(accumulatedRepositoryData[modNameLower], repoData);
|
||||
}
|
||||
}
|
||||
else if(filename.endsWith(".png", Qt::CaseInsensitive))
|
||||
images.push_back(filename);
|
||||
}
|
||||
|
||||
if (!repository.isNull())
|
||||
if (!accumulatedRepositoryData.isNull() && !repositoryFilesEnqueued)
|
||||
{
|
||||
manager->appendRepositories(repository);
|
||||
modModel->reloadRepositories();
|
||||
logGlobal->info("Installing repository: started");
|
||||
manager->setRepositoryData(accumulatedRepositoryData);
|
||||
modModel->reloadViewModel();
|
||||
accumulatedRepositoryData.clear();
|
||||
|
||||
static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json";
|
||||
JsonUtils::jsonToFile(repositoryCachePath, modStateModel->getRepositoryData());
|
||||
logGlobal->info("Installing repository: ended");
|
||||
}
|
||||
|
||||
if(!mods.empty())
|
||||
{
|
||||
logGlobal->info("Installing mods: started");
|
||||
installMods(mods);
|
||||
modStateModel->reloadLocalState();
|
||||
modModel->reloadRepositories();
|
||||
reload();
|
||||
logGlobal->info("Installing mods: ended");
|
||||
}
|
||||
|
||||
if(!maps.empty())
|
||||
{
|
||||
logGlobal->info("Installing maps: started");
|
||||
installMaps(maps);
|
||||
logGlobal->info("Installing maps: ended");
|
||||
}
|
||||
|
||||
if(!exe.empty())
|
||||
{
|
||||
ui->progressBar->setFormat(tr("Installing chronicles"));
|
||||
logGlobal->info("Installing chronicles: started");
|
||||
ui->progressBar->setFormat(tr("Installing Heroes Chronicles"));
|
||||
ui->progressWidget->setVisible(true);
|
||||
ui->pushButton->setEnabled(false);
|
||||
|
||||
float prog = 0.0;
|
||||
|
||||
@ -876,6 +817,8 @@ void CModListView::installFiles(QStringList files)
|
||||
{
|
||||
ChroniclesExtractor ce(this, [&prog](float progress) { prog = progress; });
|
||||
ce.installChronicles(exe);
|
||||
reload();
|
||||
enableModByName("chronicles");
|
||||
return true;
|
||||
});
|
||||
|
||||
@ -887,10 +830,13 @@ void CModListView::installFiles(QStringList files)
|
||||
|
||||
if(futureExtract.get())
|
||||
{
|
||||
hideProgressBar();
|
||||
ui->pushButton->setEnabled(true);
|
||||
ui->progressWidget->setVisible(false);
|
||||
//update
|
||||
modStateModel->reloadLocalState();
|
||||
modModel->reloadRepositories();
|
||||
reload();
|
||||
}
|
||||
logGlobal->info("Installing chronicles: ended");
|
||||
}
|
||||
|
||||
if(!images.empty())
|
||||
@ -914,8 +860,9 @@ void CModListView::installMods(QStringList archives)
|
||||
// uninstall old version of mod, if installed
|
||||
for(QString mod : modNames)
|
||||
{
|
||||
if(modStateModel->getMod(mod).isInstalled())
|
||||
if(modStateModel->isModExists(mod) && modStateModel->getMod(mod).isInstalled())
|
||||
{
|
||||
logGlobal->info("Uninstalling old version of mod '%s'", mod.toStdString());
|
||||
if (modStateModel->isModEnabled(mod))
|
||||
modsToEnable.push_back(mod);
|
||||
|
||||
@ -928,20 +875,30 @@ void CModListView::installMods(QStringList archives)
|
||||
}
|
||||
}
|
||||
|
||||
reload(); // FIXME: better way that won't reset selection
|
||||
|
||||
for(int i = 0; i < modNames.size(); i++)
|
||||
{
|
||||
logGlobal->info("Installing mod '%s'", modNames[i].toStdString());
|
||||
ui->progressBar->setFormat(tr("Installing mod %1").arg(modNames[i]));
|
||||
manager->installMod(modNames[i], archives[i]);
|
||||
}
|
||||
|
||||
reload();
|
||||
|
||||
if (!modsToEnable.empty())
|
||||
{
|
||||
manager->enableMods(modsToEnable);
|
||||
}
|
||||
|
||||
checkManagerErrors();
|
||||
|
||||
for(QString archive : archives)
|
||||
{
|
||||
logGlobal->info("Erasing archive '%s'", archive.toStdString());
|
||||
QFile::remove(archive);
|
||||
}
|
||||
}
|
||||
|
||||
void CModListView::installMaps(QStringList maps)
|
||||
{
|
||||
@ -949,6 +906,7 @@ void CModListView::installMaps(QStringList maps)
|
||||
|
||||
for(QString map : maps)
|
||||
{
|
||||
logGlobal->info("Importing map '%s'", map.toStdString());
|
||||
QFile(map).rename(destDir + map.section('/', -1, -1));
|
||||
}
|
||||
}
|
||||
@ -1036,11 +994,13 @@ void CModListView::on_screenshotsList_clicked(const QModelIndex & index)
|
||||
|
||||
void CModListView::doInstallMod(const QString & modName)
|
||||
{
|
||||
for(const auto & name : modStateModel->getMod(modName).getDependencies())
|
||||
for(const auto & name : getModsToInstall(modName))
|
||||
{
|
||||
auto mod = modStateModel->getMod(name);
|
||||
if(!mod.isInstalled())
|
||||
downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes());
|
||||
if(mod.isAvailable())
|
||||
downloadMod(mod);
|
||||
else if(!modStateModel->isModEnabled(name))
|
||||
enableModByName(name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1060,6 +1020,39 @@ bool CModListView::isModInstalled(const QString & modName)
|
||||
return mod.isInstalled();
|
||||
}
|
||||
|
||||
QStringList CModListView::getInstalledChronicles()
|
||||
{
|
||||
QStringList result;
|
||||
|
||||
for(const auto & modName : modStateModel->getAllMods())
|
||||
{
|
||||
auto mod = modStateModel->getMod(modName);
|
||||
if (!mod.isInstalled())
|
||||
continue;
|
||||
|
||||
if (mod.getTopParentID() != "chronicles")
|
||||
continue;
|
||||
|
||||
result += modName;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList CModListView::getUpdateableMods()
|
||||
{
|
||||
QStringList result;
|
||||
|
||||
for(const auto & modName : modStateModel->getAllMods())
|
||||
{
|
||||
auto mod = modStateModel->getMod(modName);
|
||||
if (mod.isUpdateAvailable())
|
||||
result.push_back(modName);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString CModListView::getTranslationModName(const QString & language)
|
||||
{
|
||||
for(const auto & modName : modStateModel->getAllMods())
|
||||
@ -1123,3 +1116,34 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CModListView::createNewPreset(const QString & presetName)
|
||||
{
|
||||
modStateModel->createNewPreset(presetName);
|
||||
}
|
||||
|
||||
void CModListView::deletePreset(const QString & presetName)
|
||||
{
|
||||
modStateModel->deletePreset(presetName);
|
||||
}
|
||||
|
||||
void CModListView::activatePreset(const QString & presetName)
|
||||
{
|
||||
modStateModel->activatePreset(presetName);
|
||||
reload();
|
||||
}
|
||||
|
||||
void CModListView::renamePreset(const QString & oldPresetName, const QString & newPresetName)
|
||||
{
|
||||
modStateModel->renamePreset(oldPresetName, newPresetName);
|
||||
}
|
||||
|
||||
QStringList CModListView::getAllPresets() const
|
||||
{
|
||||
return modStateModel->getAllPresets();
|
||||
}
|
||||
|
||||
QString CModListView::getActivePreset() const
|
||||
{
|
||||
return modStateModel->getActivePreset();
|
||||
}
|
||||
|
@ -36,6 +36,9 @@ class CModListView : public QWidget
|
||||
ModStateItemModel * modModel;
|
||||
CModFilterModel * filterModel;
|
||||
CDownloadManager * dlManager;
|
||||
JsonNode accumulatedRepositoryData;
|
||||
|
||||
QStringList enqueuedModDownloads;
|
||||
|
||||
void setupModModel();
|
||||
void setupFilterModel();
|
||||
@ -52,20 +55,13 @@ class CModListView : public QWidget
|
||||
// find mods unknown to mod list (not present in repo and not installed)
|
||||
QStringList findUnavailableMods(QStringList candidates);
|
||||
|
||||
void manualInstallFile(QString filePath);
|
||||
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(const ModState & mod);
|
||||
QString genModInfoText(const ModState & mod);
|
||||
|
||||
void changeEvent(QEvent *event) override;
|
||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
|
||||
public:
|
||||
explicit CModListView(QWidget * parent = nullptr);
|
||||
@ -74,6 +70,8 @@ public:
|
||||
void loadScreenshots();
|
||||
void loadRepositories();
|
||||
|
||||
void reload();
|
||||
|
||||
void disableModInfo();
|
||||
|
||||
void selectMod(const QModelIndex & index);
|
||||
@ -83,18 +81,43 @@ public:
|
||||
/// install mod by name
|
||||
void doInstallMod(const QString & modName);
|
||||
|
||||
/// update mod by name
|
||||
void doUpdateMod(const QString & modName);
|
||||
|
||||
/// returns true if mod is available in repository and can be installed
|
||||
bool isModAvailable(const QString & modName);
|
||||
|
||||
/// finds translation mod for specified languages. Returns empty string on error
|
||||
QString getTranslationModName(const QString & language);
|
||||
|
||||
/// finds all already imported Heroes Chronicles mods (if any)
|
||||
QStringList getInstalledChronicles();
|
||||
|
||||
/// finds all mods that can be updated
|
||||
QStringList getUpdateableMods();
|
||||
|
||||
void createNewPreset(const QString & presetName);
|
||||
|
||||
void deletePreset(const QString & presetName);
|
||||
|
||||
void activatePreset(const QString & presetName);
|
||||
|
||||
void renamePreset(const QString & oldPresetName, const QString & newPresetName);
|
||||
|
||||
QStringList getAllPresets() const;
|
||||
|
||||
QString getActivePreset() const;
|
||||
|
||||
/// returns true if mod is currently enabled
|
||||
bool isModEnabled(const QString & modName);
|
||||
|
||||
/// returns true if mod is currently installed
|
||||
bool isModInstalled(const QString & modName);
|
||||
|
||||
void downloadMod(const ModState & mod);
|
||||
void downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes = 0);
|
||||
void installFiles(QStringList mods);
|
||||
|
||||
public slots:
|
||||
void enableModByName(QString modName);
|
||||
void disableModByName(QString modName);
|
||||
@ -109,31 +132,17 @@ private slots:
|
||||
void hideProgressBar();
|
||||
|
||||
void on_lineEdit_textChanged(const QString & arg1);
|
||||
|
||||
void on_comboBox_currentIndexChanged(int index);
|
||||
|
||||
void on_enableButton_clicked();
|
||||
|
||||
void on_disableButton_clicked();
|
||||
|
||||
void on_updateButton_clicked();
|
||||
|
||||
void on_uninstallButton_clicked();
|
||||
|
||||
void on_installButton_clicked();
|
||||
|
||||
void on_installFromFileButton_clicked();
|
||||
|
||||
void on_pushButton_clicked();
|
||||
|
||||
void on_refreshButton_clicked();
|
||||
|
||||
void on_allModsView_activated(const QModelIndex & index);
|
||||
|
||||
void on_tabWidget_currentChanged(int index);
|
||||
|
||||
void on_screenshotsList_clicked(const QModelIndex & index);
|
||||
|
||||
void on_allModsView_doubleClicked(const QModelIndex &index);
|
||||
|
||||
private:
|
||||
|
@ -42,6 +42,9 @@
|
||||
<property name="placeholderText">
|
||||
<string>Filter</string>
|
||||
</property>
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@ -191,7 +194,9 @@
|
||||
<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css">
|
||||
p, li { white-space: pre-wrap; }
|
||||
hr { height: 1px; border-width: 0; }
|
||||
</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
|
||||
li.unchecked::marker { content: "\2610"; }
|
||||
li.checked::marker { content: "\2612"; }
|
||||
</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;">
|
||||
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu'; font-size:11pt;"><br /></p></body></html></string>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
@ -317,6 +322,9 @@ hr { height: 1px; border-width: 0; }
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="textVisible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
@ -349,41 +357,6 @@ hr { height: 1px; border-width: 0; }
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="installFromFileButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>51</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>170</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Install from file</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>icons:mod-download.png</normaloff>icons:mod-download.png</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="modButtonSpacer">
|
||||
<property name="orientation">
|
||||
|
@ -72,9 +72,9 @@ ModStateController::ModStateController(std::shared_ptr<ModStateModel> modList)
|
||||
|
||||
ModStateController::~ModStateController() = default;
|
||||
|
||||
void ModStateController::appendRepositories(const JsonNode & repomap)
|
||||
void ModStateController::setRepositoryData(const JsonNode & repomap)
|
||||
{
|
||||
modList->appendRepositories(repomap);
|
||||
modList->setRepositoryData(repomap);
|
||||
}
|
||||
|
||||
bool ModStateController::addError(QString modname, QString message)
|
||||
@ -120,6 +120,9 @@ bool ModStateController::disableMod(QString modname)
|
||||
|
||||
bool ModStateController::canInstallMod(QString modname)
|
||||
{
|
||||
if (!modList->isModExists(modname))
|
||||
return true; // for installation of unknown mods, e.g. via "Install from file" option
|
||||
|
||||
auto mod = modList->getMod(modname);
|
||||
|
||||
if(mod.isSubmod())
|
||||
@ -155,7 +158,7 @@ bool ModStateController::canEnableMod(QString modname)
|
||||
|
||||
//check for compatibility
|
||||
if(!mod.isCompatible())
|
||||
return addError(modname, tr("Mod is not compatible, please update VCMI and checkout latest mod revisions"));
|
||||
return addError(modname, tr("Mod is not compatible, please update VCMI and check the latest mod revisions"));
|
||||
|
||||
if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString())
|
||||
return addError(modname, tr("Can not enable translation mod for a different language!"));
|
||||
@ -189,9 +192,6 @@ bool ModStateController::doInstallMod(QString modname, QString archivePath)
|
||||
if(!QFile(archivePath).exists())
|
||||
return addError(modname, tr("Mod archive is missing"));
|
||||
|
||||
if(localMods.contains(modname))
|
||||
return addError(modname, tr("Mod with such name is already installed"));
|
||||
|
||||
std::vector<std::string> filesToExtract;
|
||||
QString modDirName = ::detectModArchive(archivePath, modname, filesToExtract);
|
||||
if(!modDirName.size())
|
||||
@ -235,8 +235,6 @@ bool ModStateController::doInstallMod(QString modname, QString archivePath)
|
||||
if(upperLevel != modDirName)
|
||||
removeModDir(destDir + upperLevel);
|
||||
|
||||
modList->reloadLocalState();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -251,9 +249,7 @@ bool ModStateController::doUninstallMod(QString modname)
|
||||
|
||||
QDir modFullDir(modDir);
|
||||
if(!removeModDir(modDir))
|
||||
return addError(modname, tr("Mod is located in protected directory, please remove it manually:\n") + modFullDir.absolutePath());
|
||||
|
||||
modList->reloadLocalState();
|
||||
return addError(modname, tr("Mod is located in a protected directory, please remove it manually:\n") + modFullDir.absolutePath());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -27,8 +27,6 @@ class ModStateController : public QObject, public boost::noncopyable
|
||||
bool doInstallMod(QString mod, QString archivePath);
|
||||
bool doUninstallMod(QString mod);
|
||||
|
||||
QVariantMap localMods;
|
||||
|
||||
QStringList recentErrors;
|
||||
bool addError(QString modname, QString message);
|
||||
bool removeModDir(QString mod);
|
||||
@ -37,7 +35,7 @@ public:
|
||||
ModStateController(std::shared_ptr<ModStateModel> modList);
|
||||
~ModStateController();
|
||||
|
||||
void appendRepositories(const JsonNode & repositoriesList);
|
||||
void setRepositoryData(const JsonNode & repositoriesList);
|
||||
|
||||
QStringList getErrors();
|
||||
|
||||
|
@ -32,34 +32,31 @@ QString ModStateItemModel::modIndexToName(const QModelIndex & index) const
|
||||
|
||||
QString ModStateItemModel::modTypeName(QString modTypeID) const
|
||||
{
|
||||
static const QMap<QString, QString> modTypes = {
|
||||
{"Translation", tr("Translation")},
|
||||
{"Town", tr("Town") },
|
||||
{"Test", tr("Test") },
|
||||
{"Templates", tr("Templates") },
|
||||
{"Spells", tr("Spells") },
|
||||
{"Music", tr("Music") },
|
||||
{"Maps", tr("Maps") },
|
||||
{"Sounds", tr("Sounds") },
|
||||
{"Skills", tr("Skills") },
|
||||
{"Other", tr("Other") },
|
||||
{"Objects", tr("Objects") },
|
||||
{"Mechanical", tr("Mechanics") },
|
||||
{"Mechanics", tr("Mechanics") },
|
||||
{"Themes", tr("Interface") },
|
||||
{"Interface", tr("Interface") },
|
||||
{"Heroes", tr("Heroes") },
|
||||
{"Graphic", tr("Graphical") },
|
||||
{"Graphical", tr("Graphical") },
|
||||
{"Expansion", tr("Expansion") },
|
||||
{"Creatures", tr("Creatures") },
|
||||
{"Compatibility", tr("Compatibility") },
|
||||
{"Artifacts", tr("Artifacts") },
|
||||
{"AI", tr("AI") },
|
||||
static const QStringList modTypes = {
|
||||
QT_TR_NOOP("Translation"),
|
||||
QT_TR_NOOP("Town"),
|
||||
QT_TR_NOOP("Test"),
|
||||
QT_TR_NOOP("Templates"),
|
||||
QT_TR_NOOP("Spells"),
|
||||
QT_TR_NOOP("Music"),
|
||||
QT_TR_NOOP("Maps"),
|
||||
QT_TR_NOOP("Sounds"),
|
||||
QT_TR_NOOP("Skills"),
|
||||
QT_TR_NOOP("Other"),
|
||||
QT_TR_NOOP("Objects"),
|
||||
QT_TR_NOOP("Mechanics"),
|
||||
QT_TR_NOOP("Interface"),
|
||||
QT_TR_NOOP("Heroes"),
|
||||
QT_TR_NOOP("Graphical"),
|
||||
QT_TR_NOOP("Expansion"),
|
||||
QT_TR_NOOP("Creatures"),
|
||||
QT_TR_NOOP("Compatibility") ,
|
||||
QT_TR_NOOP("Artifacts"),
|
||||
QT_TR_NOOP("AI"),
|
||||
};
|
||||
|
||||
if (modTypes.contains(modTypeID))
|
||||
return modTypes[modTypeID];
|
||||
return tr(modTypeID.toStdString().c_str());
|
||||
return tr("Other");
|
||||
}
|
||||
|
||||
@ -198,7 +195,7 @@ QVariant ModStateItemModel::headerData(int section, Qt::Orientation orientation,
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void ModStateItemModel::reloadRepositories()
|
||||
void ModStateItemModel::reloadViewModel()
|
||||
{
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
|
@ -72,7 +72,7 @@ public:
|
||||
explicit ModStateItemModel(std::shared_ptr<ModStateModel> model, QObject * parent);
|
||||
|
||||
/// CModListContainer overrides
|
||||
void reloadRepositories();
|
||||
void reloadViewModel();
|
||||
void modChanged(QString modID);
|
||||
|
||||
QVariant data(const QModelIndex & index, int role) const override;
|
||||
|
@ -11,7 +11,6 @@
|
||||
#include "modstatemodel.h"
|
||||
|
||||
#include "../../lib/filesystem/Filesystem.h"
|
||||
#include "../../lib/json/JsonUtils.h"
|
||||
#include "../../lib/modding/ModManager.h"
|
||||
|
||||
ModStateModel::ModStateModel()
|
||||
@ -22,10 +21,9 @@ ModStateModel::ModStateModel()
|
||||
|
||||
ModStateModel::~ModStateModel() = default;
|
||||
|
||||
void ModStateModel::appendRepositories(const JsonNode & repositoriesList)
|
||||
void ModStateModel::setRepositoryData(const JsonNode & repositoriesList)
|
||||
{
|
||||
JsonUtils::mergeCopy(*repositoryData, repositoriesList);
|
||||
|
||||
*repositoryData = repositoriesList;
|
||||
modManager = std::make_unique<ModManager>(*repositoryData);
|
||||
}
|
||||
|
||||
@ -128,3 +126,34 @@ QString ModStateModel::getTopParent(QString modname) const
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
void ModStateModel::createNewPreset(const QString & presetName)
|
||||
{
|
||||
modManager->createNewPreset(presetName.toStdString());
|
||||
}
|
||||
|
||||
void ModStateModel::deletePreset(const QString & presetName)
|
||||
{
|
||||
modManager->deletePreset(presetName.toStdString());
|
||||
}
|
||||
|
||||
void ModStateModel::activatePreset(const QString & presetName)
|
||||
{
|
||||
modManager->activatePreset(presetName.toStdString());
|
||||
}
|
||||
|
||||
void ModStateModel::renamePreset(const QString & oldPresetName, const QString & newPresetName)
|
||||
{
|
||||
modManager->renamePreset(oldPresetName.toStdString(), newPresetName.toStdString());
|
||||
}
|
||||
|
||||
QStringList ModStateModel::getAllPresets() const
|
||||
{
|
||||
auto result = modManager->getAllPresets();
|
||||
return stringListStdToQt(result);
|
||||
}
|
||||
|
||||
QString ModStateModel::getActivePreset() const
|
||||
{
|
||||
return QString::fromStdString(modManager->getActivePreset());
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ public:
|
||||
ModStateModel();
|
||||
~ModStateModel();
|
||||
|
||||
void appendRepositories(const JsonNode & repositoriesList);
|
||||
void setRepositoryData(const JsonNode & repositoriesList);
|
||||
void reloadLocalState();
|
||||
const JsonNode & getRepositoryData() const;
|
||||
|
||||
@ -49,4 +49,12 @@ public:
|
||||
|
||||
bool isSubmod(QString modname);
|
||||
QString getTopParent(QString modname) const;
|
||||
|
||||
void createNewPreset(const QString & presetName);
|
||||
void deletePreset(const QString & presetName);
|
||||
void activatePreset(const QString & presetName);
|
||||
void renamePreset(const QString & oldPresetName, const QString & newPresetName);
|
||||
|
||||
QStringList getAllPresets() const;
|
||||
QString getActivePreset() const;
|
||||
};
|
||||
|
@ -517,36 +517,26 @@ void CSettingsView::loadTranslation()
|
||||
if (!mainWindow)
|
||||
return;
|
||||
|
||||
QString languageName = QString::fromStdString(settings["general"]["language"].String());
|
||||
QString modName = mainWindow->getModView()->getTranslationModName(languageName);
|
||||
bool translationExists = !modName.isEmpty();
|
||||
bool translationNeeded = languageName != baseLanguage;
|
||||
bool showTranslation = translationNeeded && translationExists;
|
||||
auto translationStatus = mainWindow->getTranslationStatus();
|
||||
bool showTranslation = translationStatus == ETranslationStatus::DISABLED || translationStatus == ETranslationStatus::NOT_INSTALLLED;
|
||||
|
||||
ui->labelTranslation->setVisible(showTranslation);
|
||||
ui->labelTranslationStatus->setVisible(showTranslation);
|
||||
ui->pushButtonTranslation->setVisible(showTranslation);
|
||||
ui->pushButtonTranslation->setVisible(translationStatus != ETranslationStatus::ACTIVE);
|
||||
|
||||
if (!translationExists || !translationNeeded)
|
||||
return;
|
||||
|
||||
bool translationAvailable = mainWindow->getModView()->isModAvailable(modName);
|
||||
bool translationEnabled = mainWindow->getModView()->isModEnabled(modName);
|
||||
|
||||
ui->pushButtonTranslation->setVisible(!translationEnabled);
|
||||
|
||||
if (translationEnabled)
|
||||
if (translationStatus == ETranslationStatus::ACTIVE)
|
||||
{
|
||||
ui->labelTranslationStatus->setText(tr("Active"));
|
||||
}
|
||||
|
||||
if (!translationEnabled && !translationAvailable)
|
||||
if (translationStatus == ETranslationStatus::DISABLED)
|
||||
{
|
||||
ui->labelTranslationStatus->setText(tr("Disabled"));
|
||||
ui->pushButtonTranslation->setText(tr("Enable"));
|
||||
}
|
||||
|
||||
if (translationAvailable)
|
||||
if (translationStatus == ETranslationStatus::NOT_INSTALLLED)
|
||||
{
|
||||
ui->labelTranslationStatus->setText(tr("Not Installed"));
|
||||
ui->pushButtonTranslation->setText(tr("Install"));
|
||||
@ -614,7 +604,7 @@ void CSettingsView::on_lineEditRepositoryExtra_textEdited(const QString &arg1)
|
||||
void CSettingsView::on_spinBoxInterfaceScaling_valueChanged(int arg1)
|
||||
{
|
||||
Settings node = settings.write["video"]["resolution"]["scaling"];
|
||||
node->Float() = arg1;
|
||||
node->Float() = ui->buttonScalingAuto->isChecked() ? 0 : arg1;
|
||||
}
|
||||
|
||||
void CSettingsView::on_refreshRepositoriesButton_clicked()
|
||||
|
@ -47,7 +47,7 @@
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>-797</y>
|
||||
<y>0</y>
|
||||
<width>729</width>
|
||||
<height>1503</height>
|
||||
</rect>
|
||||
@ -1179,13 +1179,13 @@
|
||||
<item row="11" column="1" colspan="5">
|
||||
<widget class="QComboBox" name="comboBoxFullScreen">
|
||||
<property name="toolTip">
|
||||
<string>Select display mode for game
|
||||
<string>Select a display mode for the game
|
||||
|
||||
Windowed - game will run inside a window that covers part of your screen
|
||||
Windowed - the game will run inside a window that covers part of your screen.
|
||||
|
||||
Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen.
|
||||
Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution.
|
||||
|
||||
Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution.</string>
|
||||
Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution.</string>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
|
431
launcher/startGame/StartGameTab.cpp
Normal file
431
launcher/startGame/StartGameTab.cpp
Normal file
@ -0,0 +1,431 @@
|
||||
/*
|
||||
* StartGameTab.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "StartGameTab.h"
|
||||
#include "ui_StartGameTab.h"
|
||||
|
||||
#include "../mainwindow_moc.h"
|
||||
#include "../main.h"
|
||||
#include "../updatedialog_moc.h"
|
||||
|
||||
#include "../modManager/cmodlistview_moc.h"
|
||||
|
||||
#include "../../lib/filesystem/Filesystem.h"
|
||||
#include "../../lib/VCMIDirs.h"
|
||||
|
||||
void StartGameTab::changeEvent(QEvent *event)
|
||||
{
|
||||
if(event->type() == QEvent::LanguageChange)
|
||||
{
|
||||
ui->retranslateUi(this);
|
||||
refreshState();
|
||||
}
|
||||
|
||||
QWidget::changeEvent(event);
|
||||
}
|
||||
|
||||
StartGameTab::StartGameTab(QWidget * parent)
|
||||
: QWidget(parent)
|
||||
, ui(new Ui::StartGameTab)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
ui->buttonGameResume->setIcon(QIcon{":/icons/menu-game.png"}); //TODO: different icon?
|
||||
ui->buttonGameStart->setIcon(QIcon{":/icons/menu-game.png"});
|
||||
ui->buttonGameEditor->setIcon(QIcon{":/icons/menu-editor.png"});
|
||||
|
||||
refreshState();
|
||||
|
||||
ui->buttonGameResume->setVisible(false); // TODO: implement
|
||||
ui->buttonPresetExport->setVisible(false); // TODO: implement
|
||||
ui->buttonPresetImport->setVisible(false); // TODO: implement
|
||||
|
||||
#ifndef ENABLE_EDITOR
|
||||
ui->buttonGameEditor->hide();
|
||||
#endif
|
||||
}
|
||||
|
||||
StartGameTab::~StartGameTab()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
MainWindow * StartGameTab::getMainWindow()
|
||||
{
|
||||
foreach(QWidget *w, qApp->allWidgets())
|
||||
if(QMainWindow* mainWin = qobject_cast<QMainWindow*>(w))
|
||||
return dynamic_cast<MainWindow *>(mainWin);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void StartGameTab::refreshState()
|
||||
{
|
||||
refreshGameData();
|
||||
refreshUpdateStatus(EGameUpdateStatus::NOT_CHECKED);//TODO - follow automatic check on startup setting
|
||||
refreshTranslation(getMainWindow()->getTranslationStatus());
|
||||
refreshPresets();
|
||||
refreshMods();
|
||||
}
|
||||
|
||||
void StartGameTab::refreshPresets()
|
||||
{
|
||||
QSignalBlocker blocker(ui->comboBoxModPresets);
|
||||
|
||||
QStringList allPresets = getMainWindow()->getModView()->getAllPresets();
|
||||
ui->comboBoxModPresets->clear();
|
||||
ui->comboBoxModPresets->addItems(allPresets);
|
||||
ui->comboBoxModPresets->setCurrentText(getMainWindow()->getModView()->getActivePreset());
|
||||
ui->buttonPresetDelete->setVisible(allPresets.size() > 1);
|
||||
}
|
||||
|
||||
void StartGameTab::refreshGameData()
|
||||
{
|
||||
// Some players are using pirated version of the game with some of the files missing
|
||||
// leading to broken town hall menu (and possibly other dialogs)
|
||||
// Provide diagnostics to indicate problem with chair-monitor adaptor layer and not with VCMI
|
||||
static constexpr std::array potentiallyMissingFiles = {
|
||||
"Data/TpThBkDg.bmp",
|
||||
"Data/TpThBkFr.bmp",
|
||||
"Data/TpThBkIn.bmp",
|
||||
"Data/TpThBkNc.bmp",
|
||||
"Data/TpThBkSt.bmp",
|
||||
"Data/TpThBRrm.bmp",
|
||||
"Data/TpThBkCs.bmp",
|
||||
"Data/TpThBkRm.bmp",
|
||||
"Data/TpThBkTw.bmp",
|
||||
};
|
||||
|
||||
// Some players for some reason don't have AB expansion campaign files
|
||||
static constexpr std::array armaggedonBladeCampaigns = {
|
||||
"DATA/AB",
|
||||
"DATA/BLOOD",
|
||||
"DATA/SLAYER",
|
||||
"DATA/FESTIVAL",
|
||||
"DATA/FIRE",
|
||||
"DATA/FOOL",
|
||||
};
|
||||
|
||||
bool missingSoundtrack = !CResourceHandler::get()->existsResource(AudioPath::builtin("Music/MainMenu"));
|
||||
bool missingVideoFiles = !CResourceHandler::get()->existsResource(VideoPath::builtin("Video/H3Intro")) && !CResourceHandler::get()->existsResource(ResourcePath("Video/H3Intro", EResType::VIDEO_LOW_QUALITY));
|
||||
bool missingGameFiles = false;
|
||||
bool missingCampaings = false;
|
||||
|
||||
for (const auto & filename : potentiallyMissingFiles)
|
||||
missingGameFiles &= !CResourceHandler::get()->existsResource(ImagePath::builtin(filename));
|
||||
|
||||
for (const auto & filename : armaggedonBladeCampaigns)
|
||||
missingCampaings &= !CResourceHandler::get()->existsResource(ResourcePath(filename, EResType::CAMPAIGN));
|
||||
|
||||
ui->labelMissingCampaigns->setVisible(missingCampaings);
|
||||
ui->labelMissingFiles->setVisible(missingGameFiles);
|
||||
ui->labelMissingVideo->setVisible(missingVideoFiles);
|
||||
ui->labelMissingSoundtrack->setVisible(missingSoundtrack);
|
||||
|
||||
ui->buttonMissingCampaignsHelp->setVisible(missingCampaings);
|
||||
ui->buttonMissingFilesHelp->setVisible(missingGameFiles);
|
||||
ui->buttonMissingVideoHelp->setVisible(missingVideoFiles);
|
||||
ui->buttonMissingSoundtrackHelp->setVisible(missingSoundtrack);
|
||||
}
|
||||
|
||||
void StartGameTab::refreshTranslation(ETranslationStatus status)
|
||||
{
|
||||
ui->buttonInstallTranslation->setVisible(status == ETranslationStatus::NOT_INSTALLLED);
|
||||
ui->buttonInstallTranslationHelp->setVisible(status == ETranslationStatus::NOT_INSTALLLED);
|
||||
|
||||
ui->buttonActivateTranslation->setVisible(status == ETranslationStatus::NOT_INSTALLLED);
|
||||
ui->buttonActivateTranslationHelp->setVisible(status == ETranslationStatus::NOT_INSTALLLED);
|
||||
}
|
||||
|
||||
void StartGameTab::refreshMods()
|
||||
{
|
||||
constexpr int chroniclesCount = 8;
|
||||
QStringList updateableMods = getMainWindow()->getModView()->getUpdateableMods();
|
||||
QStringList chroniclesMods = getMainWindow()->getModView()->getInstalledChronicles();
|
||||
|
||||
ui->buttonUpdateMods->setText(tr("Update %n mods", "", updateableMods.size()));
|
||||
ui->buttonUpdateMods->setVisible(!updateableMods.empty());
|
||||
ui->buttonUpdateModsHelp->setVisible(!updateableMods.empty());
|
||||
|
||||
ui->labelChronicles->setText(tr("Heroes Chronicles:\n%n/%1 installed", "", chroniclesMods.size()).arg(chroniclesCount));
|
||||
ui->labelChronicles->setVisible(chroniclesMods.size() != chroniclesCount);
|
||||
ui->buttonChroniclesHelp->setVisible(chroniclesMods.size() != chroniclesCount);
|
||||
}
|
||||
|
||||
void StartGameTab::refreshUpdateStatus(EGameUpdateStatus status)
|
||||
{
|
||||
QString availableVersion; // TODO
|
||||
|
||||
ui->labelTitleEngine->setText("VCMI " VCMI_VERSION_STRING);
|
||||
ui->buttonUpdateCheck->setVisible(status == EGameUpdateStatus::NOT_CHECKED);
|
||||
ui->labelUpdateNotFound->setVisible(status == EGameUpdateStatus::NO_UPDATE);
|
||||
ui->labelUpdateAvailable->setVisible(status == EGameUpdateStatus::UPDATE_AVAILABLE);
|
||||
ui->buttonOpenChangelog->setVisible(status == EGameUpdateStatus::UPDATE_AVAILABLE);
|
||||
ui->buttonOpenDownloads->setVisible(status == EGameUpdateStatus::UPDATE_AVAILABLE);
|
||||
|
||||
if (status == EGameUpdateStatus::UPDATE_AVAILABLE)
|
||||
ui->labelUpdateAvailable->setText(tr("Update to %1 available").arg(availableVersion));
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonGameStart_clicked()
|
||||
{
|
||||
getMainWindow()->hide();
|
||||
startGame({});
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonOpenChangelog_clicked()
|
||||
{
|
||||
QDesktopServices::openUrl(QUrl("https://vcmi.eu/ChangeLog/"));
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonOpenDownloads_clicked()
|
||||
{
|
||||
QDesktopServices::openUrl(QUrl("https://vcmi.eu/download/"));
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonUpdateCheck_clicked()
|
||||
{
|
||||
UpdateDialog::showUpdateDialog(true);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonGameEditor_clicked()
|
||||
{
|
||||
getMainWindow()->hide();
|
||||
startEditor({});
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonImportFiles_clicked()
|
||||
{
|
||||
const auto & importFunctor = [this]
|
||||
{
|
||||
#ifndef VCMI_MOBILE
|
||||
QString filter =
|
||||
tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json *.exe);;" +
|
||||
tr("Maps") + " (*.h3m *.vmap);;" +
|
||||
tr("Campaigns") + " (*.h3c *.vcmp);;" +
|
||||
tr("Configs") + " (*.json);;" +
|
||||
tr("Mods") + " (*.zip);;" +
|
||||
tr("Gog files") + " (*.exe)";
|
||||
#else
|
||||
//Workaround for sometimes incorrect mime for some extensions (e.g. for exe)
|
||||
QString filter = tr("All files (*.*)");
|
||||
#endif
|
||||
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns, gog files) to install..."), QDir::homePath(), filter);
|
||||
|
||||
for(const auto & file : files)
|
||||
{
|
||||
logGlobal->info("Importing file %s", file.toStdString());
|
||||
getMainWindow()->manualInstallFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
// iOS can't display modal dialogs when called directly on button press
|
||||
// https://bugreports.qt.io/browse/QTBUG-98651
|
||||
QTimer::singleShot(0, this, importFunctor);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonInstallTranslation_clicked()
|
||||
{
|
||||
if (getMainWindow()->getTranslationStatus() == ETranslationStatus::NOT_INSTALLLED)
|
||||
{
|
||||
QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String());
|
||||
QString modName = getMainWindow()->getModView()->getTranslationModName(preferredlanguage);
|
||||
getMainWindow()->getModView()->doInstallMod(modName);
|
||||
}
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonActivateTranslation_clicked()
|
||||
{
|
||||
QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String());
|
||||
QString modName = getMainWindow()->getModView()->getTranslationModName(preferredlanguage);
|
||||
getMainWindow()->getModView()->enableModByName(modName);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonUpdateMods_clicked()
|
||||
{
|
||||
QStringList updateableMods = getMainWindow()->getModView()->getUpdateableMods();
|
||||
|
||||
getMainWindow()->switchToModsTab();
|
||||
|
||||
for (const auto & modName : updateableMods)
|
||||
getMainWindow()->getModView()->doUpdateMod(modName);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonHelpImportFiles_clicked()
|
||||
{
|
||||
QString message = tr(
|
||||
"This option allows you to import additional data files into your VCMI installation. "
|
||||
"At the moment, following options are supported:\n\n"
|
||||
" - Heroes III Maps (.h3m or .vmap).\n"
|
||||
" - Heroes III Campaigns (.h3c or .vcmp).\n"
|
||||
" - Heroes III Chronicles using offline backup installer from GOG.com (.exe).\n"
|
||||
" - VCMI mods in zip format (.zip)\n"
|
||||
" - VCMI configuration files (.json)\n"
|
||||
);
|
||||
|
||||
QMessageBox::information(this, ui->buttonImportFiles->text(), message);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonInstallTranslationHelp_clicked()
|
||||
{
|
||||
QString message = tr(
|
||||
"Your Heroes III version uses different language. "
|
||||
"VCMI provides translations of the game into various languages that you can use. "
|
||||
"Use this option to automatically install such translation to your language."
|
||||
);
|
||||
QMessageBox::information(this, ui->buttonInstallTranslation->text(), message);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonActivateTranslationHelp_clicked()
|
||||
{
|
||||
QString message = tr(
|
||||
"Translation of Heroes III into your language is installed, but has been turned off. "
|
||||
"Use this option to enable it."
|
||||
);
|
||||
|
||||
QMessageBox::information(this, ui->buttonActivateTranslation->text(), message);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonUpdateModsHelp_clicked()
|
||||
{
|
||||
QString message = tr(
|
||||
"A new version of some of the mods that you have installed is now available in mod repository. "
|
||||
"Use this option to automatically update all your mods to latest version.\n\n"
|
||||
"WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. "
|
||||
"You many want to postpone mod update until you finish any of your ongoing games."
|
||||
);
|
||||
|
||||
QMessageBox::information(this, ui->buttonUpdateMods->text(), message);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonChroniclesHelp_clicked()
|
||||
{
|
||||
QString message = tr(
|
||||
"If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog "
|
||||
"to import Heroes Chronicles data into VCMI as custom campaigns.\n"
|
||||
"To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, "
|
||||
"select 'Import files' option and select downloaded file. "
|
||||
"This will generate and install mod for VCMI that contains imported chronicles"
|
||||
);
|
||||
|
||||
QMessageBox::information(this, ui->labelChronicles->text(), message);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonMissingSoundtrackHelp_clicked()
|
||||
{
|
||||
QString message = tr(
|
||||
"VCMI has detected that Heroes III music files are missing from your installation. "
|
||||
"VCMI will run, but in-game music will not be available.\n\n"
|
||||
"To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually "
|
||||
"or reinstall VCMI and re-import Heroes III data files"
|
||||
);
|
||||
QMessageBox::information(this, ui->labelMissingSoundtrack->text(), message);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonMissingVideoHelp_clicked()
|
||||
{
|
||||
QString message = tr(
|
||||
"VCMI has detected that Heroes III video files are missing from your installation. "
|
||||
"VCMI will run, but in-game cutscenes will not be available.\n\n"
|
||||
"To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually "
|
||||
"or reinstall VCMI and re-import Heroes III data files"
|
||||
);
|
||||
QMessageBox::information(this, ui->labelMissingVideo->text(), message);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonMissingFilesHelp_clicked()
|
||||
{
|
||||
QString message = tr(
|
||||
"VCMI has detected that some of Heroes III data files are missing from your installation. "
|
||||
"You may attempt to run VCMI, but game may not work as expected or crash.\n\n"
|
||||
"To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. "
|
||||
"VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com"
|
||||
);
|
||||
QMessageBox::information(this, ui->labelMissingFiles->text(), message);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonMissingCampaignsHelp_clicked()
|
||||
{
|
||||
QString message = tr(
|
||||
"VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. "
|
||||
"VCMI will work, but Armageddon's Blade campaigns will not be available.\n\n"
|
||||
"To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually "
|
||||
"or reinstall VCMI and re-import Heroes III data files"
|
||||
);
|
||||
QMessageBox::information(this, ui->labelMissingCampaigns->text(), message);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonPresetExport_clicked()
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonPresetImport_clicked()
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonPresetNew_clicked()
|
||||
{
|
||||
bool ok;
|
||||
QString presetName = QInputDialog::getText(
|
||||
this,
|
||||
ui->buttonPresetNew->text(),
|
||||
tr("Enter preset name:"),
|
||||
QLineEdit::Normal,
|
||||
QString(),
|
||||
&ok);
|
||||
|
||||
if (ok && !presetName.isEmpty())
|
||||
{
|
||||
getMainWindow()->getModView()->createNewPreset(presetName);
|
||||
getMainWindow()->getModView()->activatePreset(presetName);
|
||||
refreshPresets();
|
||||
}
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonPresetDelete_clicked()
|
||||
{
|
||||
QString activePresetBefore = getMainWindow()->getModView()->getActivePreset();
|
||||
QStringList allPresets = getMainWindow()->getModView()->getAllPresets();
|
||||
|
||||
allPresets.removeAll(activePresetBefore);
|
||||
if (!allPresets.empty())
|
||||
{
|
||||
getMainWindow()->getModView()->activatePreset(allPresets.front());
|
||||
getMainWindow()->getModView()->deletePreset(activePresetBefore);
|
||||
refreshPresets();
|
||||
}
|
||||
}
|
||||
|
||||
void StartGameTab::on_comboBoxModPresets_currentTextChanged(const QString &presetName)
|
||||
{
|
||||
getMainWindow()->getModView()->activatePreset(presetName);
|
||||
}
|
||||
|
||||
void StartGameTab::on_buttonPresetRename_clicked()
|
||||
{
|
||||
QString currentName = getMainWindow()->getModView()->getActivePreset();
|
||||
|
||||
bool ok;
|
||||
QString newName = QInputDialog::getText(
|
||||
this,
|
||||
ui->buttonPresetNew->text(),
|
||||
tr("Rename preset '%1' to:").arg(currentName),
|
||||
QLineEdit::Normal,
|
||||
currentName,
|
||||
&ok);
|
||||
|
||||
if (ok && !newName.isEmpty())
|
||||
{
|
||||
getMainWindow()->getModView()->renamePreset(currentName, newName);
|
||||
refreshPresets();
|
||||
}
|
||||
}
|
||||
|
83
launcher/startGame/StartGameTab.h
Normal file
83
launcher/startGame/StartGameTab.h
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* StartGameTab.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class StartGameTab;
|
||||
}
|
||||
|
||||
enum class EGameUpdateStatus : int8_t
|
||||
{
|
||||
NOT_CHECKED,
|
||||
NO_UPDATE,
|
||||
UPDATE_AVAILABLE
|
||||
};
|
||||
|
||||
enum class ETranslationStatus : int8_t;
|
||||
|
||||
class MainWindow;
|
||||
|
||||
class StartGameTab : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
MainWindow * getMainWindow();
|
||||
|
||||
void refreshUpdateStatus(EGameUpdateStatus status);
|
||||
void refreshTranslation(ETranslationStatus status);
|
||||
void refreshMods();
|
||||
void refreshPresets();
|
||||
void refreshGameData();
|
||||
|
||||
void changeEvent(QEvent *event) override;
|
||||
public:
|
||||
explicit StartGameTab(QWidget * parent = nullptr);
|
||||
~StartGameTab();
|
||||
|
||||
void refreshState();
|
||||
|
||||
private slots:
|
||||
void on_buttonGameStart_clicked();
|
||||
void on_buttonOpenChangelog_clicked();
|
||||
void on_buttonOpenDownloads_clicked();
|
||||
void on_buttonUpdateCheck_clicked();
|
||||
void on_buttonGameEditor_clicked();
|
||||
void on_buttonImportFiles_clicked();
|
||||
void on_buttonInstallTranslation_clicked();
|
||||
void on_buttonActivateTranslation_clicked();
|
||||
void on_buttonUpdateMods_clicked();
|
||||
void on_buttonHelpImportFiles_clicked();
|
||||
void on_buttonInstallTranslationHelp_clicked();
|
||||
void on_buttonActivateTranslationHelp_clicked();
|
||||
void on_buttonUpdateModsHelp_clicked();
|
||||
void on_buttonChroniclesHelp_clicked();
|
||||
void on_buttonMissingSoundtrackHelp_clicked();
|
||||
void on_buttonMissingVideoHelp_clicked();
|
||||
void on_buttonMissingFilesHelp_clicked();
|
||||
void on_buttonMissingCampaignsHelp_clicked();
|
||||
|
||||
void on_buttonPresetExport_clicked();
|
||||
|
||||
void on_buttonPresetImport_clicked();
|
||||
|
||||
void on_buttonPresetNew_clicked();
|
||||
|
||||
void on_buttonPresetDelete_clicked();
|
||||
|
||||
void on_comboBoxModPresets_currentTextChanged(const QString &arg1);
|
||||
|
||||
void on_buttonPresetRename_clicked();
|
||||
|
||||
private:
|
||||
Ui::StartGameTab * ui;
|
||||
};
|
857
launcher/startGame/StartGameTab.ui
Normal file
857
launcher/startGame/StartGameTab.ui
Normal file
@ -0,0 +1,857 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>StartGameTab</class>
|
||||
<widget class="QWidget" name="StartGameTab">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>757</width>
|
||||
<height>372</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3" columnstretch="1,1,1">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelTitleDataFiles">
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Game Data Files</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="labelTitleModPreset">
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Mod Preset</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="labelTitleEngine">
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QScrollArea" name="scrollArea_2">
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>246</width>
|
||||
<height>350</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="labelMissingFiles">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Unsupported or corrupted game data detected!</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="buttonHelpImportFiles">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QPushButton" name="buttonMissingFilesHelp">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="labelChronicles">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="buttonUpdateModsHelp">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="buttonInstallTranslationHelp">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="buttonInstallTranslation">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Install Translation</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="labelMissingCampaigns">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Armaggedon's Blade campaigns are missing!</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="labelMissingVideo">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>No video files detected!</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QPushButton" name="buttonImportFiles">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Import files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QPushButton" name="buttonUpdateMods">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QPushButton" name="buttonMissingSoundtrackHelp">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QPushButton" name="buttonMissingCampaignsHelp">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="labelMissingSoundtrack">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>No soundtrack detected!</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QPushButton" name="buttonMissingVideoHelp">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="buttonActivateTranslation">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Activate Translation</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QPushButton" name="buttonChroniclesHelp">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="buttonActivateTranslationHelp">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>247</width>
|
||||
<height>350</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout" rowstretch="1,0,0,0,0,0,0,0,0,0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="buttonPresetExport">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Export to Clipboard</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QPushButton" name="buttonPresetNew">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Create New Preset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QComboBox" name="comboBoxModPresets">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QPushButton" name="buttonPresetDelete">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Delete Current Preset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QPushButton" name="buttonPresetImport">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Import from Clipboard</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QPushButton" name="buttonPresetRename">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Rename Current Preset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QScrollArea" name="scrollArea_3">
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents_3">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>272</width>
|
||||
<height>350</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelUpdateNotFound">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>You are using the latest version</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelUpdateAvailable">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUpdateCheck">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Check For Updates</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonOpenDownloads">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Go to Downloads Page</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonOpenChangelog">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Go to Changelog Page</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QToolButton" name="buttonGameResume">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>80</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Resume</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="buttonGameEditor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>80</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Editor</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="buttonGameStart">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>80</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Play</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -98,7 +98,7 @@ void UpdateDialog::loadFromJson(const JsonNode & node)
|
||||
node["changeLog"].getType() != JsonNode::JsonType::DATA_STRING ||
|
||||
node["downloadLinks"].getType() != JsonNode::JsonType::DATA_STRUCT) //we need at least one link - other are optional
|
||||
{
|
||||
ui->plainTextEdit->setPlainText(tr("Cannot read JSON from url or incorrect JSON data"));
|
||||
ui->plainTextEdit->setPlainText(tr("Cannot read JSON from URL or incorrect JSON data"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1400,8 +1400,10 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio
|
||||
case EventCondition::TRANSPORT:
|
||||
{
|
||||
const auto * t = getTown(condition.objectID);
|
||||
return (t->visitingHero && t->visitingHero->getOwner() == player && t->visitingHero->hasArt(condition.objectType.as<ArtifactID>())) ||
|
||||
(t->garrisonHero && t->garrisonHero->getOwner() == player && t->garrisonHero->hasArt(condition.objectType.as<ArtifactID>()));
|
||||
bool garrisonedWon = t->garrisonHero && t->garrisonHero->getOwner() == player && t->garrisonHero->hasArt(condition.objectType.as<ArtifactID>());
|
||||
bool visitingWon = t->visitingHero && t->visitingHero->getOwner() == player && t->visitingHero->hasArt(condition.objectType.as<ArtifactID>());
|
||||
|
||||
return garrisonedWon || visitingWon;
|
||||
}
|
||||
case EventCondition::DAYS_PASSED:
|
||||
{
|
||||
@ -1436,6 +1438,9 @@ PlayerColor CGameState::checkForStandardWin() const
|
||||
TeamID winnerTeam = TeamID::NO_TEAM;
|
||||
for(const auto & elem : players)
|
||||
{
|
||||
if(elem.second.status == EPlayerStatus::WINNER)
|
||||
return elem.second.color;
|
||||
|
||||
if(elem.second.status == EPlayerStatus::INGAME && elem.first.isValidPlayer())
|
||||
{
|
||||
if(supposedWinner == PlayerColor::NEUTRAL)
|
||||
|
@ -1923,7 +1923,8 @@ int CGHeroInstance::getBasePrimarySkillValue(PrimarySkill which) const
|
||||
{
|
||||
std::string cachingStr = "type_PRIMARY_SKILL_base_" + std::to_string(static_cast<int>(which));
|
||||
auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL));
|
||||
return valOfBonuses(selector, cachingStr);
|
||||
auto minSkillValue = VLC->engineSettings()->getVector(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS)[which.getNum()];
|
||||
return std::max(valOfBonuses(selector, cachingStr), minSkillValue);
|
||||
}
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
||||
|
@ -120,8 +120,7 @@ const JsonNode & ModDescription::getLocalizedValue(const std::string & keyName)
|
||||
|
||||
const JsonNode & ModDescription::getValue(const std::string & keyName) const
|
||||
{
|
||||
const JsonNode & localValue = getLocalValue(keyName);
|
||||
if (localValue.isNull())
|
||||
if (!isInstalled() || isUpdateAvailable())
|
||||
return getRepositoryValue(keyName);
|
||||
else
|
||||
return getLocalValue(keyName);
|
||||
|
@ -163,7 +163,7 @@ ModsPresetState::ModsPresetState()
|
||||
CResourceHandler::get("local")->createResource(settingsPath.getOriginalName() + ".json");
|
||||
}
|
||||
|
||||
if(modConfig["presets"].isNull())
|
||||
if(modConfig["presets"].isNull() || modConfig["presets"].Struct().empty())
|
||||
{
|
||||
modConfig["activePreset"] = JsonNode("default");
|
||||
if(modConfig["activeMods"].isNull())
|
||||
@ -171,6 +171,10 @@ ModsPresetState::ModsPresetState()
|
||||
else
|
||||
importInitialPreset(); // 1.5 format import
|
||||
}
|
||||
|
||||
auto allPresets = getAllPresets();
|
||||
if (!vstd::contains(allPresets, modConfig["activePreset"].String()))
|
||||
modConfig["activePreset"] = JsonNode(allPresets.front());
|
||||
}
|
||||
|
||||
void ModsPresetState::createInitialPreset()
|
||||
@ -326,6 +330,61 @@ void ModsPresetState::saveConfigurationState() const
|
||||
file << modConfig.toCompactString();
|
||||
}
|
||||
|
||||
void ModsPresetState::createNewPreset(const std::string & presetName)
|
||||
{
|
||||
if (modConfig["presets"][presetName].isNull())
|
||||
modConfig["presets"][presetName]["mods"].Vector().emplace_back("vcmi");
|
||||
}
|
||||
|
||||
void ModsPresetState::deletePreset(const std::string & presetName)
|
||||
{
|
||||
if (modConfig["presets"].Struct().size() < 2)
|
||||
throw std::runtime_error("Unable to delete last preset!");
|
||||
|
||||
modConfig["presets"].Struct().erase(presetName);
|
||||
}
|
||||
|
||||
void ModsPresetState::activatePreset(const std::string & presetName)
|
||||
{
|
||||
if (modConfig["presets"].Struct().count(presetName) == 0)
|
||||
throw std::runtime_error("Unable to activate non-exinsting preset!");
|
||||
|
||||
modConfig["activePreset"].String() = presetName;
|
||||
}
|
||||
|
||||
void ModsPresetState::renamePreset(const std::string & oldPresetName, const std::string & newPresetName)
|
||||
{
|
||||
if (oldPresetName == newPresetName)
|
||||
throw std::runtime_error("Unable to rename preset to the same name!");
|
||||
|
||||
if (modConfig["presets"].Struct().count(oldPresetName) == 0)
|
||||
throw std::runtime_error("Unable to rename non-existing last preset!");
|
||||
|
||||
if (modConfig["presets"].Struct().count(newPresetName) != 0)
|
||||
throw std::runtime_error("Unable to rename preset - preset with such name already exists!");
|
||||
|
||||
modConfig["presets"][newPresetName] = modConfig["presets"][oldPresetName];
|
||||
modConfig["presets"].Struct().erase(oldPresetName);
|
||||
|
||||
if (modConfig["activePreset"].String() == oldPresetName)
|
||||
modConfig["activePreset"].String() = newPresetName;
|
||||
}
|
||||
|
||||
std::vector<std::string> ModsPresetState::getAllPresets() const
|
||||
{
|
||||
std::vector<std::string> presets;
|
||||
|
||||
for (const auto & preset : modConfig["presets"].Struct())
|
||||
presets.push_back(preset.first);
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
std::string ModsPresetState::getActivePreset() const
|
||||
{
|
||||
return modConfig["activePreset"].String();
|
||||
}
|
||||
|
||||
ModsStorage::ModsStorage(const std::vector<TModID> & modsToLoad, const JsonNode & repositoryList)
|
||||
{
|
||||
JsonNode coreModConfig(JsonPath::builtin("config/gameConfig.json"));
|
||||
@ -595,7 +654,7 @@ void ModManager::updatePreset(const ModDependenciesResolver & testResolver)
|
||||
for (const auto & modID : newBrokenMods)
|
||||
{
|
||||
const auto & mod = getModDescription(modID);
|
||||
if (vstd::contains(newActiveMods, mod.getTopParentID()))
|
||||
if (mod.getTopParentID().empty() || vstd::contains(newActiveMods, mod.getTopParentID()))
|
||||
modsPreset->setModActive(modID, false);
|
||||
}
|
||||
|
||||
@ -703,4 +762,38 @@ void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStora
|
||||
brokenMods.insert(brokenMods.end(), modsToResolve.begin(), modsToResolve.end());
|
||||
}
|
||||
|
||||
void ModManager::createNewPreset(const std::string & presetName)
|
||||
{
|
||||
modsPreset->createNewPreset(presetName);
|
||||
modsPreset->saveConfigurationState();
|
||||
}
|
||||
|
||||
void ModManager::deletePreset(const std::string & presetName)
|
||||
{
|
||||
modsPreset->deletePreset(presetName);
|
||||
modsPreset->saveConfigurationState();
|
||||
}
|
||||
|
||||
void ModManager::activatePreset(const std::string & presetName)
|
||||
{
|
||||
modsPreset->activatePreset(presetName);
|
||||
modsPreset->saveConfigurationState();
|
||||
}
|
||||
|
||||
void ModManager::renamePreset(const std::string & oldPresetName, const std::string & newPresetName)
|
||||
{
|
||||
modsPreset->renamePreset(oldPresetName, newPresetName);
|
||||
modsPreset->saveConfigurationState();
|
||||
}
|
||||
|
||||
std::vector<std::string> ModManager::getAllPresets() const
|
||||
{
|
||||
return modsPreset->getAllPresets();
|
||||
}
|
||||
|
||||
std::string ModManager::getActivePreset() const
|
||||
{
|
||||
return modsPreset->getActivePreset();
|
||||
}
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
||||
|
@ -50,6 +50,14 @@ class ModsPresetState : boost::noncopyable
|
||||
public:
|
||||
ModsPresetState();
|
||||
|
||||
void createNewPreset(const std::string & presetName);
|
||||
void deletePreset(const std::string & presetName);
|
||||
void activatePreset(const std::string & presetName);
|
||||
void renamePreset(const std::string & oldPresetName, const std::string & newPresetName);
|
||||
|
||||
std::vector<std::string> getAllPresets() const;
|
||||
std::string getActivePreset() const;
|
||||
|
||||
void setModActive(const TModID & modName, bool isActive);
|
||||
|
||||
void addRootMod(const TModID & modName);
|
||||
@ -139,6 +147,14 @@ public:
|
||||
|
||||
void tryEnableMods(const TModList & modList);
|
||||
void tryDisableMod(const TModID & modName);
|
||||
|
||||
void createNewPreset(const std::string & presetName);
|
||||
void deletePreset(const std::string & presetName);
|
||||
void activatePreset(const std::string & presetName);
|
||||
void renamePreset(const std::string & oldPresetName, const std::string & newPresetName);
|
||||
|
||||
std::vector<std::string> getAllPresets() const;
|
||||
std::string getActivePreset() const;
|
||||
};
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
||||
|
@ -199,7 +199,11 @@ void NetworkConnection::close()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
socket->close(ec);
|
||||
#if BOOST_VERSION >= 108700
|
||||
timer->cancel();
|
||||
#else
|
||||
timer->cancel(ec);
|
||||
#endif
|
||||
|
||||
//NOTE: ignoring error code, intended
|
||||
}
|
||||
|
@ -15,7 +15,11 @@
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
#if BOOST_VERSION >= 108700
|
||||
using NetworkContext = boost::asio::io_context;
|
||||
#else
|
||||
using NetworkContext = boost::asio::io_service;
|
||||
#endif
|
||||
using NetworkSocket = boost::asio::ip::tcp::socket;
|
||||
using NetworkAcceptor = boost::asio::ip::tcp::acceptor;
|
||||
using NetworkBuffer = boost::asio::streambuf;
|
||||
|
@ -1193,6 +1193,17 @@ 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<FlaggableMapObject*>(obj);
|
||||
if(flaggableObject)
|
||||
{
|
||||
flaggableObject->markAsDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
if(obj->ID == Obj::HERO) //remove beaten hero
|
||||
{
|
||||
auto * beatenHero = dynamic_cast<CGHeroInstance *>(obj);
|
||||
@ -1251,18 +1262,6 @@ void RemoveObject::applyGs(CGameState *gs)
|
||||
}
|
||||
}
|
||||
|
||||
if(obj->getOwner().isValidPlayer())
|
||||
{
|
||||
gs->getPlayerState(obj->getOwner())->removeOwnedObject(obj); //object removed via map event or hero got beaten
|
||||
|
||||
FlaggableMapObject* flaggableObject = dynamic_cast<FlaggableMapObject*>(obj);
|
||||
if(flaggableObject)
|
||||
{
|
||||
flaggableObject->markAsDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
gs->map->instanceNames.erase(obj->instanceName);
|
||||
gs->map->objects[objectID.getNum()].dellNull();
|
||||
gs->map->calculateGuardingGreaturePositions();//FIXME: excessive, update only affected tiles
|
||||
|
BIN
mapeditor/icons/document-open-recent.png
Normal file
BIN
mapeditor/icons/document-open-recent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
@ -67,10 +67,10 @@ void HeroSkillsWidget::on_checkBox_toggled(bool checked)
|
||||
|
||||
void HeroSkillsWidget::obtainData()
|
||||
{
|
||||
ui->attack->setValue(hero.getPrimSkillLevel(PrimarySkill::ATTACK));
|
||||
ui->defence->setValue(hero.getPrimSkillLevel(PrimarySkill::DEFENSE));
|
||||
ui->power->setValue(hero.getPrimSkillLevel(PrimarySkill::SPELL_POWER));
|
||||
ui->knowledge->setValue(hero.getPrimSkillLevel(PrimarySkill::KNOWLEDGE));
|
||||
ui->attack->setValue(hero.getBasePrimarySkillValue(PrimarySkill::ATTACK));
|
||||
ui->defence->setValue(hero.getBasePrimarySkillValue(PrimarySkill::DEFENSE));
|
||||
ui->power->setValue(hero.getBasePrimarySkillValue(PrimarySkill::SPELL_POWER));
|
||||
ui->knowledge->setValue(hero.getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE));
|
||||
|
||||
if(!hero.secSkills.empty() && hero.secSkills.front().first.getNum() == -1)
|
||||
return;
|
||||
|
@ -16,6 +16,8 @@
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QFileInfo>
|
||||
#include <QDialog>
|
||||
#include <QListWidget>
|
||||
|
||||
#include "../lib/VCMIDirs.h"
|
||||
#include "../lib/VCMI_Lib.h"
|
||||
@ -222,6 +224,8 @@ MainWindow::MainWindow(QWidget* parent) :
|
||||
ui->toolFill->setIcon(QIcon{":/icons/tool-fill.png"});
|
||||
ui->toolSelect->setIcon(QIcon{":/icons/tool-select.png"});
|
||||
ui->actionOpen->setIcon(QIcon{":/icons/document-open.png"});
|
||||
ui->actionOpenRecent->setIcon(QIcon{":/icons/document-open-recent.png"});
|
||||
ui->menuOpenRecent->setIcon(QIcon{":/icons/document-open-recent.png"});
|
||||
ui->actionSave->setIcon(QIcon{":/icons/document-save.png"});
|
||||
ui->actionNew->setIcon(QIcon{":/icons/document-new.png"});
|
||||
ui->actionLevel->setIcon(QIcon{":/icons/toggle-underground.png"});
|
||||
@ -265,6 +269,8 @@ MainWindow::MainWindow(QWidget* parent) :
|
||||
scenePreview = new QGraphicsScene(this);
|
||||
ui->objectPreview->setScene(scenePreview);
|
||||
|
||||
connect(ui->actionOpenRecentMore, &QAction::triggered, this, &MainWindow::on_actionOpenRecent_triggered);
|
||||
|
||||
//loading objects
|
||||
loadObjectsTree();
|
||||
|
||||
@ -412,9 +418,21 @@ bool MainWindow::openMap(const QString & filenameSelect)
|
||||
|
||||
filename = filenameSelect;
|
||||
initializeMap(controller.map()->version != EMapFormat::VCMI);
|
||||
|
||||
updateRecentMenu(filenameSelect);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::updateRecentMenu(const QString & filenameSelect) {
|
||||
QSettings s(Ui::teamName, Ui::appName);
|
||||
QStringList recentFiles = s.value(recentlyOpenedFilesSetting).toStringList();
|
||||
recentFiles.removeAll(filenameSelect);
|
||||
recentFiles.prepend(filenameSelect);
|
||||
constexpr int maxRecentFiles = 10;
|
||||
s.setValue(recentlyOpenedFilesSetting, QStringList(recentFiles.mid(0, maxRecentFiles)));
|
||||
}
|
||||
|
||||
void MainWindow::on_actionOpen_triggered()
|
||||
{
|
||||
if(!getAnswerAboutUnsavedChanges())
|
||||
@ -429,6 +447,91 @@ void MainWindow::on_actionOpen_triggered()
|
||||
openMap(filenameSelect);
|
||||
}
|
||||
|
||||
void MainWindow::on_actionOpenRecent_triggered()
|
||||
{
|
||||
QSettings s(Ui::teamName, Ui::appName);
|
||||
QStringList recentFiles = s.value(recentlyOpenedFilesSetting).toStringList();
|
||||
|
||||
class RecentFileDialog : public QDialog
|
||||
{
|
||||
|
||||
public:
|
||||
RecentFileDialog(const QStringList& recentFiles, QWidget *parent)
|
||||
: QDialog(parent), layout(new QVBoxLayout(this)), listWidget(new QListWidget(this))
|
||||
{
|
||||
|
||||
setWindowTitle(tr("Recently Opened Files"));
|
||||
setMinimumWidth(600);
|
||||
|
||||
connect(listWidget, &QListWidget::itemActivated, this, [this](QListWidgetItem *item)
|
||||
{
|
||||
accept();
|
||||
});
|
||||
|
||||
for (const QString &file : recentFiles)
|
||||
{
|
||||
QListWidgetItem *item = new QListWidgetItem(file);
|
||||
listWidget->addItem(item);
|
||||
}
|
||||
|
||||
// Select most recent items by default.
|
||||
// This enables a "CTRL+R => Enter"-workflow instead of "CTRL+R => 'mouse click on first item'"
|
||||
if(listWidget->count() > 0)
|
||||
{
|
||||
listWidget->item(0)->setSelected(true);
|
||||
}
|
||||
|
||||
layout->setSizeConstraint(QLayout::SetMaximumSize);
|
||||
layout->addWidget(listWidget);
|
||||
}
|
||||
|
||||
QString getSelectedFilePath() const
|
||||
{
|
||||
return listWidget->currentItem()->text();
|
||||
}
|
||||
|
||||
private:
|
||||
QVBoxLayout * layout;
|
||||
QListWidget * listWidget;
|
||||
};
|
||||
|
||||
RecentFileDialog d(recentFiles, this);
|
||||
if(d.exec() == QDialog::Accepted && getAnswerAboutUnsavedChanges())
|
||||
{
|
||||
openMap(d.getSelectedFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_menuOpenRecent_aboutToShow()
|
||||
{
|
||||
// Clear all actions except "More...", lest the list will grow with each
|
||||
// showing of the list
|
||||
for (QAction* action : ui->menuOpenRecent->actions()) {
|
||||
if (action != ui->actionOpenRecentMore) {
|
||||
ui->menuOpenRecent->removeAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
QSettings s(Ui::teamName, Ui::appName);
|
||||
QStringList recentFiles = s.value(recentlyOpenedFilesSetting).toStringList();
|
||||
|
||||
// Dynamically populate menuOpenRecent with one action per file.
|
||||
for (const QString & file : recentFiles) {
|
||||
QAction *action = new QAction(file, this);
|
||||
ui->menuOpenRecent->insertAction(ui->actionOpenRecentMore, action);
|
||||
connect(action, &QAction::triggered, this, [this, file]() {
|
||||
if(!getAnswerAboutUnsavedChanges())
|
||||
return;
|
||||
openMap(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Finally add a separator between recent entries and "More..."
|
||||
if(recentFiles.size() > 0) {
|
||||
ui->menuOpenRecent->insertSeparator(ui->actionOpenRecentMore);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::saveMap()
|
||||
{
|
||||
if(!controller.map())
|
||||
@ -534,7 +637,7 @@ void MainWindow::roadOrRiverButtonClicked(ui8 type, bool isRoad)
|
||||
controller.commitRoadOrRiverChange(mapLevel, type, isRoad);
|
||||
}
|
||||
|
||||
void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool staticOnly)
|
||||
void MainWindow::addGroupIntoCatalog(const QString & groupName, bool staticOnly)
|
||||
{
|
||||
auto knownObjects = VLC->objtypeh->knownObjects();
|
||||
for(auto ID : knownObjects)
|
||||
@ -546,13 +649,13 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool staticO
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCustomName, bool staticOnly, int ID)
|
||||
void MainWindow::addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID)
|
||||
{
|
||||
QStandardItem * itemGroup = nullptr;
|
||||
auto itms = objectsModel.findItems(QString::fromStdString(groupName));
|
||||
auto itms = objectsModel.findItems(groupName);
|
||||
if(itms.empty())
|
||||
{
|
||||
itemGroup = new QStandardItem(QString::fromStdString(groupName));
|
||||
itemGroup = new QStandardItem(groupName);
|
||||
objectsModel.appendRow(itemGroup);
|
||||
}
|
||||
else
|
||||
@ -684,138 +787,158 @@ void MainWindow::loadObjectsTree()
|
||||
ui->treeView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
connect(ui->treeView->selectionModel(), SIGNAL(currentChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(treeViewSelected(const QModelIndex &, const QModelIndex &)));
|
||||
|
||||
//groups
|
||||
enum GroupCat { TOWNS, OBJECTS, HEROES, ARTIFACTS, RESOURCES, BANKS, DWELLINGS, GROUNDS, TELEPORTS, MINES, TRIGGERS, MONSTERS, QUESTS, WOG_OBJECTS, OBSTACLES, OTHER };
|
||||
QMap<GroupCat, QString> groups = {
|
||||
{ TOWNS, tr("Towns") },
|
||||
{ OBJECTS, tr("Objects") },
|
||||
{ HEROES, tr("Heroes") },
|
||||
{ ARTIFACTS, tr("Artifacts") },
|
||||
{ RESOURCES, tr("Resources") },
|
||||
{ BANKS, tr("Banks") },
|
||||
{ DWELLINGS, tr("Dwellings") },
|
||||
{ GROUNDS, tr("Grounds") },
|
||||
{ TELEPORTS, tr("Teleports") },
|
||||
{ MINES, tr("Mines") },
|
||||
{ TRIGGERS, tr("Triggers") },
|
||||
{ MONSTERS, tr("Monsters") },
|
||||
{ QUESTS, tr("Quests") },
|
||||
{ WOG_OBJECTS, tr("Wog Objects") },
|
||||
{ OBSTACLES, tr("Obstacles") },
|
||||
{ OTHER, tr("Other") },
|
||||
};
|
||||
|
||||
//adding objects
|
||||
addGroupIntoCatalog("TOWNS", false, false, Obj::TOWN);
|
||||
addGroupIntoCatalog("TOWNS", false, false, Obj::RANDOM_TOWN);
|
||||
addGroupIntoCatalog("TOWNS", true, false, Obj::SHIPYARD);
|
||||
addGroupIntoCatalog("TOWNS", true, false, Obj::GARRISON);
|
||||
addGroupIntoCatalog("TOWNS", true, false, Obj::GARRISON2);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::ARENA);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::BUOY);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::CARTOGRAPHER);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::SWAN_POND);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::COVER_OF_DARKNESS);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::CORPSE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::FAERIE_RING);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::FOUNTAIN_OF_FORTUNE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::FOUNTAIN_OF_YOUTH);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::GARDEN_OF_REVELATION);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::HILL_FORT);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::IDOL_OF_FORTUNE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::LIBRARY_OF_ENLIGHTENMENT);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::LIGHTHOUSE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOOL_OF_MAGIC);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::MAGIC_SPRING);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::MAGIC_WELL);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::MERCENARY_CAMP);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::MERMAID);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::MYSTICAL_GARDEN);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::OASIS);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::LEAN_TO);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::OBELISK);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::REDWOOD_OBSERVATORY);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::PILLAR_OF_FIRE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::STAR_AXIS);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::RALLY_FLAG);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::WATERING_HOLE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOLAR);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_INCANTATION);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_GESTURE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_THOUGHT);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::SIRENS);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::STABLES);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::TAVERN);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::TEMPLE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::DEN_OF_THIEVES);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::LEARNING_STONE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::TREE_OF_KNOWLEDGE);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::WAGON);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOOL_OF_WAR);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::WAR_MACHINE_FACTORY);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::WARRIORS_TOMB);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::WITCH_HUT);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::SANCTUARY);
|
||||
addGroupIntoCatalog("OBJECTS", true, false, Obj::MARLETTO_TOWER);
|
||||
addGroupIntoCatalog("HEROES", true, false, Obj::PRISON);
|
||||
addGroupIntoCatalog("HEROES", false, false, Obj::HERO);
|
||||
addGroupIntoCatalog("HEROES", false, false, Obj::RANDOM_HERO);
|
||||
addGroupIntoCatalog("HEROES", false, false, Obj::HERO_PLACEHOLDER);
|
||||
addGroupIntoCatalog("HEROES", false, false, Obj::BOAT);
|
||||
addGroupIntoCatalog("ARTIFACTS", true, false, Obj::ARTIFACT);
|
||||
addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_ART);
|
||||
addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_TREASURE_ART);
|
||||
addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_MINOR_ART);
|
||||
addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_MAJOR_ART);
|
||||
addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_RELIC_ART);
|
||||
addGroupIntoCatalog("ARTIFACTS", true, false, Obj::SPELL_SCROLL);
|
||||
addGroupIntoCatalog("ARTIFACTS", true, false, Obj::PANDORAS_BOX);
|
||||
addGroupIntoCatalog("RESOURCES", true, false, Obj::RANDOM_RESOURCE);
|
||||
addGroupIntoCatalog("RESOURCES", false, false, Obj::RESOURCE);
|
||||
addGroupIntoCatalog("RESOURCES", true, false, Obj::SEA_CHEST);
|
||||
addGroupIntoCatalog("RESOURCES", true, false, Obj::TREASURE_CHEST);
|
||||
addGroupIntoCatalog("RESOURCES", true, false, Obj::CAMPFIRE);
|
||||
addGroupIntoCatalog("RESOURCES", true, false, Obj::SHIPWRECK_SURVIVOR);
|
||||
addGroupIntoCatalog("RESOURCES", true, false, Obj::FLOTSAM);
|
||||
addGroupIntoCatalog("BANKS", true, false, Obj::CREATURE_BANK);
|
||||
addGroupIntoCatalog("BANKS", true, false, Obj::DRAGON_UTOPIA);
|
||||
addGroupIntoCatalog("BANKS", true, false, Obj::CRYPT);
|
||||
addGroupIntoCatalog("BANKS", true, false, Obj::DERELICT_SHIP);
|
||||
addGroupIntoCatalog("BANKS", true, false, Obj::PYRAMID);
|
||||
addGroupIntoCatalog("BANKS", true, false, Obj::SHIPWRECK);
|
||||
addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR1);
|
||||
addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR2);
|
||||
addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR3);
|
||||
addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR4);
|
||||
addGroupIntoCatalog("DWELLINGS", true, false, Obj::REFUGEE_CAMP);
|
||||
addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING);
|
||||
addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING_LVL);
|
||||
addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING_FACTION);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::CURSED_GROUND1);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_PLAINS1);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::CLOVER_FIELD);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::CURSED_GROUND2);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::EVIL_FOG);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::FAVORABLE_WINDS);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::FIERY_FIELDS);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::HOLY_GROUNDS);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::LUCID_POOLS);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_CLOUDS);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_PLAINS2);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::ROCKLANDS);
|
||||
addGroupIntoCatalog("GROUNDS", true, false, Obj::HOLE);
|
||||
addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_ONE_WAY_ENTRANCE);
|
||||
addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_ONE_WAY_EXIT);
|
||||
addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_TWO_WAY);
|
||||
addGroupIntoCatalog("TELEPORTS", true, false, Obj::SUBTERRANEAN_GATE);
|
||||
addGroupIntoCatalog("TELEPORTS", true, false, Obj::WHIRLPOOL);
|
||||
addGroupIntoCatalog("MINES", true, false, Obj::MINE);
|
||||
addGroupIntoCatalog("MINES", false, false, Obj::ABANDONED_MINE);
|
||||
addGroupIntoCatalog("MINES", true, false, Obj::WINDMILL);
|
||||
addGroupIntoCatalog("MINES", true, false, Obj::WATER_WHEEL);
|
||||
addGroupIntoCatalog("TRIGGERS", true, false, Obj::EVENT);
|
||||
addGroupIntoCatalog("TRIGGERS", true, false, Obj::GRAIL);
|
||||
addGroupIntoCatalog("TRIGGERS", true, false, Obj::SIGN);
|
||||
addGroupIntoCatalog("TRIGGERS", true, false, Obj::OCEAN_BOTTLE);
|
||||
addGroupIntoCatalog("MONSTERS", false, false, Obj::MONSTER);
|
||||
addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER);
|
||||
addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L1);
|
||||
addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L2);
|
||||
addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L3);
|
||||
addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L4);
|
||||
addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L5);
|
||||
addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L6);
|
||||
addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L7);
|
||||
addGroupIntoCatalog("QUESTS", true, false, Obj::SEER_HUT);
|
||||
addGroupIntoCatalog("QUESTS", true, false, Obj::BORDER_GATE);
|
||||
addGroupIntoCatalog("QUESTS", true, false, Obj::QUEST_GUARD);
|
||||
addGroupIntoCatalog("QUESTS", true, false, Obj::HUT_OF_MAGI);
|
||||
addGroupIntoCatalog("QUESTS", true, false, Obj::EYE_OF_MAGI);
|
||||
addGroupIntoCatalog("QUESTS", true, false, Obj::BORDERGUARD);
|
||||
addGroupIntoCatalog("QUESTS", true, false, Obj::KEYMASTER);
|
||||
addGroupIntoCatalog("wog object", true, false, Obj::WOG_OBJECT);
|
||||
addGroupIntoCatalog("OBSTACLES", true);
|
||||
addGroupIntoCatalog("OTHER", false);
|
||||
addGroupIntoCatalog(groups[TOWNS], false, false, Obj::TOWN);
|
||||
addGroupIntoCatalog(groups[TOWNS], false, false, Obj::RANDOM_TOWN);
|
||||
addGroupIntoCatalog(groups[TOWNS], true, false, Obj::SHIPYARD);
|
||||
addGroupIntoCatalog(groups[TOWNS], true, false, Obj::GARRISON);
|
||||
addGroupIntoCatalog(groups[TOWNS], true, false, Obj::GARRISON2);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::ARENA);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::BUOY);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::CARTOGRAPHER);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SWAN_POND);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::COVER_OF_DARKNESS);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::CORPSE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::FAERIE_RING);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::FOUNTAIN_OF_FORTUNE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::FOUNTAIN_OF_YOUTH);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::GARDEN_OF_REVELATION);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::HILL_FORT);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::IDOL_OF_FORTUNE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::LIBRARY_OF_ENLIGHTENMENT);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::LIGHTHOUSE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SCHOOL_OF_MAGIC);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MAGIC_SPRING);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MAGIC_WELL);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MERCENARY_CAMP);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MERMAID);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MYSTICAL_GARDEN);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::OASIS);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::LEAN_TO);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::OBELISK);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::REDWOOD_OBSERVATORY);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::PILLAR_OF_FIRE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::STAR_AXIS);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::RALLY_FLAG);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WATERING_HOLE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SCHOLAR);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SHRINE_OF_MAGIC_INCANTATION);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SHRINE_OF_MAGIC_GESTURE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SHRINE_OF_MAGIC_THOUGHT);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SIRENS);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::STABLES);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::TAVERN);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::TEMPLE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::DEN_OF_THIEVES);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::LEARNING_STONE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::TREE_OF_KNOWLEDGE);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WAGON);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SCHOOL_OF_WAR);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WAR_MACHINE_FACTORY);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WARRIORS_TOMB);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WITCH_HUT);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SANCTUARY);
|
||||
addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MARLETTO_TOWER);
|
||||
addGroupIntoCatalog(groups[HEROES], true, false, Obj::PRISON);
|
||||
addGroupIntoCatalog(groups[HEROES], false, false, Obj::HERO);
|
||||
addGroupIntoCatalog(groups[HEROES], false, false, Obj::RANDOM_HERO);
|
||||
addGroupIntoCatalog(groups[HEROES], false, false, Obj::HERO_PLACEHOLDER);
|
||||
addGroupIntoCatalog(groups[HEROES], false, false, Obj::BOAT);
|
||||
addGroupIntoCatalog(groups[ARTIFACTS], true, false, Obj::ARTIFACT);
|
||||
addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_ART);
|
||||
addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_TREASURE_ART);
|
||||
addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_MINOR_ART);
|
||||
addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_MAJOR_ART);
|
||||
addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_RELIC_ART);
|
||||
addGroupIntoCatalog(groups[ARTIFACTS], true, false, Obj::SPELL_SCROLL);
|
||||
addGroupIntoCatalog(groups[ARTIFACTS], true, false, Obj::PANDORAS_BOX);
|
||||
addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::RANDOM_RESOURCE);
|
||||
addGroupIntoCatalog(groups[RESOURCES], false, false, Obj::RESOURCE);
|
||||
addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::SEA_CHEST);
|
||||
addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::TREASURE_CHEST);
|
||||
addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::CAMPFIRE);
|
||||
addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::SHIPWRECK_SURVIVOR);
|
||||
addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::FLOTSAM);
|
||||
addGroupIntoCatalog(groups[BANKS], true, false, Obj::CREATURE_BANK);
|
||||
addGroupIntoCatalog(groups[BANKS], true, false, Obj::DRAGON_UTOPIA);
|
||||
addGroupIntoCatalog(groups[BANKS], true, false, Obj::CRYPT);
|
||||
addGroupIntoCatalog(groups[BANKS], true, false, Obj::DERELICT_SHIP);
|
||||
addGroupIntoCatalog(groups[BANKS], true, false, Obj::PYRAMID);
|
||||
addGroupIntoCatalog(groups[BANKS], true, false, Obj::SHIPWRECK);
|
||||
addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::CREATURE_GENERATOR1);
|
||||
addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::CREATURE_GENERATOR2);
|
||||
addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::CREATURE_GENERATOR3);
|
||||
addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::CREATURE_GENERATOR4);
|
||||
addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::REFUGEE_CAMP);
|
||||
addGroupIntoCatalog(groups[DWELLINGS], false, false, Obj::RANDOM_DWELLING);
|
||||
addGroupIntoCatalog(groups[DWELLINGS], false, false, Obj::RANDOM_DWELLING_LVL);
|
||||
addGroupIntoCatalog(groups[DWELLINGS], false, false, Obj::RANDOM_DWELLING_FACTION);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::CURSED_GROUND1);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::MAGIC_PLAINS1);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::CLOVER_FIELD);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::CURSED_GROUND2);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::EVIL_FOG);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::FAVORABLE_WINDS);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::FIERY_FIELDS);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::HOLY_GROUNDS);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::LUCID_POOLS);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::MAGIC_CLOUDS);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::MAGIC_PLAINS2);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::ROCKLANDS);
|
||||
addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::HOLE);
|
||||
addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::MONOLITH_ONE_WAY_ENTRANCE);
|
||||
addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::MONOLITH_ONE_WAY_EXIT);
|
||||
addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::MONOLITH_TWO_WAY);
|
||||
addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::SUBTERRANEAN_GATE);
|
||||
addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::WHIRLPOOL);
|
||||
addGroupIntoCatalog(groups[MINES], true, false, Obj::MINE);
|
||||
addGroupIntoCatalog(groups[MINES], false, false, Obj::ABANDONED_MINE);
|
||||
addGroupIntoCatalog(groups[MINES], true, false, Obj::WINDMILL);
|
||||
addGroupIntoCatalog(groups[MINES], true, false, Obj::WATER_WHEEL);
|
||||
addGroupIntoCatalog(groups[TRIGGERS], true, false, Obj::EVENT);
|
||||
addGroupIntoCatalog(groups[TRIGGERS], true, false, Obj::GRAIL);
|
||||
addGroupIntoCatalog(groups[TRIGGERS], true, false, Obj::SIGN);
|
||||
addGroupIntoCatalog(groups[TRIGGERS], true, false, Obj::OCEAN_BOTTLE);
|
||||
addGroupIntoCatalog(groups[MONSTERS], false, false, Obj::MONSTER);
|
||||
addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER);
|
||||
addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L1);
|
||||
addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L2);
|
||||
addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L3);
|
||||
addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L4);
|
||||
addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L5);
|
||||
addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L6);
|
||||
addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L7);
|
||||
addGroupIntoCatalog(groups[QUESTS], true, false, Obj::SEER_HUT);
|
||||
addGroupIntoCatalog(groups[QUESTS], true, false, Obj::BORDER_GATE);
|
||||
addGroupIntoCatalog(groups[QUESTS], true, false, Obj::QUEST_GUARD);
|
||||
addGroupIntoCatalog(groups[QUESTS], true, false, Obj::HUT_OF_MAGI);
|
||||
addGroupIntoCatalog(groups[QUESTS], true, false, Obj::EYE_OF_MAGI);
|
||||
addGroupIntoCatalog(groups[QUESTS], true, false, Obj::BORDERGUARD);
|
||||
addGroupIntoCatalog(groups[QUESTS], true, false, Obj::KEYMASTER);
|
||||
addGroupIntoCatalog(groups[WOG_OBJECTS], true, false, Obj::WOG_OBJECT);
|
||||
addGroupIntoCatalog(groups[OBSTACLES], true);
|
||||
addGroupIntoCatalog(groups[OTHER], false);
|
||||
}
|
||||
catch(const std::exception &)
|
||||
{
|
||||
|
@ -28,6 +28,7 @@ class MainWindow : public QMainWindow
|
||||
const QString mainWindowSizeSetting = "MainWindow/Size";
|
||||
const QString mainWindowPositionSetting = "MainWindow/Position";
|
||||
const QString lastDirectorySetting = "MainWindow/Directory";
|
||||
const QString recentlyOpenedFilesSetting = "MainWindow/RecentlyOpenedFiles";
|
||||
|
||||
#ifdef ENABLE_QT_TRANSLATIONS
|
||||
QTranslator translator;
|
||||
@ -59,6 +60,10 @@ public:
|
||||
private slots:
|
||||
void on_actionOpen_triggered();
|
||||
|
||||
void on_actionOpenRecent_triggered();
|
||||
|
||||
void on_menuOpenRecent_aboutToShow();
|
||||
|
||||
void on_actionSave_as_triggered();
|
||||
|
||||
void on_actionNew_triggered();
|
||||
@ -153,8 +158,8 @@ public slots:
|
||||
|
||||
private:
|
||||
void preparePreview(const QModelIndex & index);
|
||||
void addGroupIntoCatalog(const std::string & groupName, bool staticOnly);
|
||||
void addGroupIntoCatalog(const std::string & groupName, bool useCustomName, bool staticOnly, int ID);
|
||||
void addGroupIntoCatalog(const QString & groupName, bool staticOnly);
|
||||
void addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID);
|
||||
|
||||
QAction * getActionPlayer(const PlayerColor &);
|
||||
|
||||
@ -170,6 +175,8 @@ private:
|
||||
|
||||
void parseCommandLine(ExtractionOptions & extractionOptions);
|
||||
|
||||
void updateRecentMenu(const QString & filenameSelect);
|
||||
|
||||
private:
|
||||
Ui::MainWindow * ui;
|
||||
ObjectBrowserProxyModel * objectBrowser = nullptr;
|
||||
|
@ -58,8 +58,15 @@
|
||||
<property name="title">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuOpenRecent">
|
||||
<property name="title">
|
||||
<string>Open Recent</string>
|
||||
</property>
|
||||
<addaction name="actionOpenRecentMore"/>
|
||||
</widget>
|
||||
<addaction name="actionNew"/>
|
||||
<addaction name="actionOpen"/>
|
||||
<addaction name="menuOpenRecent"/>
|
||||
<addaction name="actionSave"/>
|
||||
<addaction name="actionSave_as"/>
|
||||
<addaction name="actionExport"/>
|
||||
@ -133,6 +140,7 @@
|
||||
</attribute>
|
||||
<addaction name="actionNew"/>
|
||||
<addaction name="actionOpen"/>
|
||||
<addaction name="actionOpenRecent"/>
|
||||
<addaction name="actionSave"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionUndo"/>
|
||||
@ -1019,6 +1027,19 @@
|
||||
<string notr="true">Ctrl+O</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionOpenRecent">
|
||||
<property name="text">
|
||||
<string>Open Recent</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionOpenRecentMore">
|
||||
<property name="text">
|
||||
<string>More...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string notr="true">Ctrl+R</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSave">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
|
@ -43,7 +43,7 @@ void LoseConditions::initialize(MapController & c)
|
||||
|
||||
for(auto & s : conditionStringsLose)
|
||||
{
|
||||
ui->loseComboBox->addItem(QString::fromStdString(s));
|
||||
ui->loseComboBox->addItem(tr(s.c_str()));
|
||||
}
|
||||
ui->standardLoseCheck->setChecked(false);
|
||||
|
||||
|
@ -48,7 +48,7 @@ void VictoryConditions::initialize(MapController & c)
|
||||
|
||||
for(auto & s : conditionStringsWin)
|
||||
{
|
||||
ui->victoryComboBox->addItem(QString::fromStdString(s));
|
||||
ui->victoryComboBox->addItem(tr(s.c_str()));
|
||||
}
|
||||
ui->standardVictoryCheck->setChecked(false);
|
||||
ui->onlyForHumansCheck->setChecked(false);
|
||||
|
@ -24,7 +24,7 @@ PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent)
|
||||
ui->setupUi(this);
|
||||
|
||||
//set colors and teams
|
||||
ui->teamId->addItem("No team", QVariant(TeamID::NO_TEAM));
|
||||
ui->teamId->addItem(tr("No team"), QVariant(TeamID::NO_TEAM));
|
||||
for(int i = 0, index = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
|
||||
{
|
||||
if(i == playerId || !controller.map()->players[i].canAnyonePlay())
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user