pax_global_header00006660000000000000000000000064134233200720014506gustar00rootroot0000000000000052 comment=b769d62c2b9235a64956571bab894a877c6f8c7e vcmi-0.99+git20190113.f06c8a87/000077500000000000000000000000001342332007200151525ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/.gitignore000066400000000000000000000012111342332007200171350ustar00rootroot00000000000000/client/vcmiclient /server/vcmiserver /launcher/vcmilauncher /launcher/vcmilauncher_automoc.cpp *.dll *.exe *.depend *.o *.a *.so *.res *.layout *.pro.user *.pro.user.* *.swp *.h.gch *~ *.autosave.* /CMakeLists.txt.user CMakeCache.txt CMakeFiles Makefile cmake_install.cmake install_manifest.txt *_cotire.cmake cotire moc_*.cpp qrc_*.cpp ui_*.h /CPackConfig.cmake /CPackSourceConfig.cmake build-* CMakeLists.txt.user.* Doxyfile doc/* VCMI_VS11.sdf *.ipch VCMI_VS11.opensdf # Visual Studio *.suo *.user /AI/*/RD /client/RD /launcher/RD /lib/RD /lib/minizip/RD /scripting/erm/RD /server/RD /test/RD /VCMI_VS11.VC.opendb /AI/FuzzyLite.lib /deps .vs/ vcmi-0.99+git20190113.f06c8a87/.gitmodules000066400000000000000000000001421342332007200173240ustar00rootroot00000000000000[submodule "test/googletest"] path = test/googletest url = https://github.com/google/googletest vcmi-0.99+git20190113.f06c8a87/.travis.yml000066400000000000000000000051461342332007200172710ustar00rootroot00000000000000language: cpp os: - linux - osx dist: trusty sudo: required env: matrix: - ignore=this global: - secure: NMg+qtQB4DIZ/KqlDeIn3K7A7Ydksdpnbv6Ha9n4bSSA0AT8wlPwbHXvQmiR8qYs6cnz4fyY6NVcBe7X3bdR8jWyPNAS0l0QByqG12q3dBpEtNNn0X5u/GS3wHse5+ObNAF9a83+xACTQj2UdxqHgJ3LFGzdBpQt3kLsc8NDnn8= matrix: exclude: - env: ignore=this include: - os: linux compiler: clang env: VCMI_PLATFORM='linux' REAL_CC=clang-3.6 REAL_CXX=clang++-3.6 PACKAGE=clang-3.6 SUPPORT=libstdc++-4.8-dev - os: linux compiler: clang env: VCMI_PLATFORM='linux' REAL_CC=clang-3.4 REAL_CXX=clang++-3.4 PACKAGE=clang-3.4 SUPPORT=libstdc++-4.8-dev - os: linux compiler: gcc env: VCMI_PLATFORM='linux' REAL_CC=gcc-4.8 REAL_CXX=g++-4.8 PACKAGE=g++-4.8 SUPPORT= - os: linux env: VCMI_PLATFORM='mxe' MXE_TARGET=i686-w64-mingw32.shared VCMI_CMAKE_FLAGS='-DENABLE_TEST=0' sudo: required - os: osx env: VCMI_PLATFORM='mac' addons: apt: update: true coverity_scan: project: name: vcmi/vcmi description: Build submitted via Travis CI notification_email: coverity@arseniyshestakov.com build_command_prepend: cov-configure --compiler clang-3.6 --comptype clangcc && cov-configure --comptype clangcxx --compiler clang++-3.6 && cmake -G Ninja .. -DCMAKE_BUILD_TYPE=DEBUG -DENABLE_LAUNCHER=0 build_command: ninja -j 3 branch_pattern: coverity_scan notifications: email: recipients: - noreply@vcmi.eu on_success: change on_failure: always slack: secure: KHXFe14FFKtw5mErWbj730+utqy7i/3AUobWfAMAGvWI5sJYlhbBU+KvvCoD2SlRQg3mQqgwVw8NBJF1Mffs7WcRmrFFFmuMqZxFLAfKBd3T0CxWpAGfnfNgDmlfV4OfEgQWk1pakEPOymhxbbmLUuCjykZDuTcioxAk0UAHDwY= before_install: - test $TRAVIS_BRANCH != coverity_scan -o ${TRAVIS_JOB_NUMBER##*.} = 1 || exit 0 - . $TRAVIS_BUILD_DIR/CI/$VCMI_PLATFORM/before_install.sh before_script: - mkdir build - cd build - if [[ $TRAVIS_BRANCH != 'coverity_scan' ]]; then source $TRAVIS_BUILD_DIR/CI/get_package_name.sh; if [[ $VCMI_PLATFORM == 'mxe' ]]; then /usr/lib/mxe/usr/bin/i686-w64-mingw32.shared-cmake -G Ninja .. $VCMI_CMAKE_FLAGS -DPACKAGE_NAME_SUFFIX:STRING="$VCMI_PACKAGE_NAME_SUFFIX" -DPACKAGE_FILE_NAME:STRING="$VCMI_PACKAGE_FILE_NAME"; else cmake -G Ninja .. $VCMI_CMAKE_FLAGS -DPACKAGE_NAME_SUFFIX:STRING="$VCMI_PACKAGE_NAME_SUFFIX" -DPACKAGE_FILE_NAME:STRING="$VCMI_PACKAGE_FILE_NAME"; fi fi script: - test $TRAVIS_BRANCH != coverity_scan || exit 0 - ninja after_success: - test $TRAVIS_BRANCH != coverity_scan || exit 0 - . $TRAVIS_BUILD_DIR/CI/$VCMI_PLATFORM/upload_package.sh vcmi-0.99+git20190113.f06c8a87/AI/000077500000000000000000000000001342332007200154435ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/000077500000000000000000000000001342332007200170705ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/AttackPossibility.cpp000066400000000000000000000055461342332007200232500ustar00rootroot00000000000000/* * AttackPossibility.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "AttackPossibility.h" AttackPossibility::AttackPossibility(BattleHex tile_, const BattleAttackInfo & attack_) : tile(tile_), attack(attack_) { } int64_t AttackPossibility::damageDiff() const { //TODO: use target priority from HypotheticBattle const auto dealtDmgValue = damageDealt; const auto receivedDmgValue = damageReceived; int64_t diff = 0; //friendly fire or not if(attack.attacker->unitSide() == attack.defender->unitSide()) diff = -dealtDmgValue - receivedDmgValue; else diff = dealtDmgValue - receivedDmgValue; //mind control auto actualSide = getCbc()->playerToSide(getCbc()->battleGetOwner(attack.attacker)); if(actualSide && actualSide.get() != attack.attacker->unitSide()) diff = -diff; return diff; } int64_t AttackPossibility::attackValue() const { return damageDiff() + tacticImpact; } AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInfo, BattleHex hex) { const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; static const auto selectorBlocksRetaliation = Selector::type(Bonus::BLOCKS_RETALIATION); const bool counterAttacksBlocked = attackInfo.attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); AttackPossibility ap(hex, attackInfo); ap.attackerState = attackInfo.attacker->acquireState(); const int totalAttacks = ap.attackerState->getTotalAttacks(attackInfo.shooting); if(!attackInfo.shooting) ap.attackerState->setPosition(hex); auto defenderState = attackInfo.defender->acquireState(); ap.affectedUnits.push_back(defenderState); for(int i = 0; i < totalAttacks; i++) { TDmgRange retaliation(0,0); auto attackDmg = getCbc()->battleEstimateDamage(ap.attack, &retaliation); vstd::amin(attackDmg.first, defenderState->getAvailableHealth()); vstd::amin(attackDmg.second, defenderState->getAvailableHealth()); vstd::amin(retaliation.first, ap.attackerState->getAvailableHealth()); vstd::amin(retaliation.second, ap.attackerState->getAvailableHealth()); ap.damageDealt += (attackDmg.first + attackDmg.second) / 2; ap.attackerState->afterAttack(attackInfo.shooting, false); //FIXME: use ranged retaliation if(!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked) { ap.damageReceived += (retaliation.first + retaliation.second) / 2; defenderState->afterAttack(attackInfo.shooting, true); } ap.attackerState->damage(ap.damageReceived); defenderState->damage(ap.damageDealt); if(!ap.attackerState->alive() || !defenderState->alive()) break; } //TODO other damage related to attack (eg. fire shield and other abilities) return ap; } vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/AttackPossibility.h000066400000000000000000000016361342332007200227110ustar00rootroot00000000000000/* * AttackPossibility.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/battle/CUnitState.h" #include "../../CCallback.h" #include "common.h" #include "StackWithBonuses.h" class AttackPossibility { public: BattleHex tile; //tile from which we attack BattleAttackInfo attack; std::shared_ptr attackerState; std::vector> affectedUnits; int64_t damageDealt = 0; int64_t damageReceived = 0; //usually by counter-attack int64_t tacticImpact = 0; AttackPossibility(BattleHex tile_, const BattleAttackInfo & attack_); int64_t damageDiff() const; int64_t attackValue() const; static AttackPossibility evaluate(const BattleAttackInfo & attackInfo, BattleHex hex); }; vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/BattleAI.cbp000066400000000000000000000066521342332007200212140ustar00rootroot00000000000000 vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/BattleAI.cpp000066400000000000000000000362541342332007200212330ustar00rootroot00000000000000/* * BattleAI.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "BattleAI.h" #include #include "StackWithBonuses.h" #include "EnemyInfo.h" #include "PossibleSpellcast.h" #include "../../lib/CStopWatch.h" #include "../../lib/CThreadHelper.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/CStack.h"//todo: remove #define LOGL(text) print(text) #define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) class RNGStub : public vstd::RNG { public: vstd::TRandI64 getInt64Range(int64_t lower, int64_t upper) override { return [=]()->int64_t { return (lower + upper)/2; }; } vstd::TRand getDoubleRange(double lower, double upper) override { return [=]()->double { return (lower + upper)/2; }; } }; enum class SpellTypes { ADVENTURE, BATTLE, OTHER }; SpellTypes spellType(const CSpell * spell) { if(!spell->isCombatSpell() || spell->isCreatureAbility()) return SpellTypes::OTHER; if(spell->isOffensiveSpell() || spell->hasEffects() || spell->hasBattleEffects()) return SpellTypes::BATTLE; return SpellTypes::OTHER; } CBattleAI::CBattleAI() : side(-1), wasWaitingForRealize(false), wasUnlockingGs(false) { } CBattleAI::~CBattleAI() { if(cb) { //Restore previous state of CB - it may be shared with the main AI (like VCAI) cb->waitTillRealize = wasWaitingForRealize; cb->unlockGsWhenWaiting = wasUnlockingGs; } } void CBattleAI::init(std::shared_ptr CB) { setCbc(CB); cb = CB; playerID = *CB->getPlayerID(); //TODO should be sth in callback wasWaitingForRealize = cb->waitTillRealize; wasUnlockingGs = CB->unlockGsWhenWaiting; CB->waitTillRealize = true; CB->unlockGsWhenWaiting = false; } BattleAction CBattleAI::activeStack( const CStack * stack ) { LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()) ; setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) try { if(stack->type->idNumber == CreatureID::CATAPULT) return useCatapult(stack); if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->hasBonusOfType(Bonus::HEALER)) { auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); std::map woundHpToStack; for(auto stack : healingTargets) if(auto woundHp = stack->MaxHealth() - stack->getFirstHPleft()) woundHpToStack[woundHp] = stack; if(woundHpToStack.empty()) return BattleAction::makeDefend(stack); else return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack } attemptCastingSpell(); if(auto ret = getCbc()->battleIsFinished()) { //spellcast may finish battle //send special preudo-action BattleAction cancel; cancel.actionType = EActionType::CANCEL; return cancel; } if(auto action = considerFleeingOrSurrendering()) return *action; //best action is from effective owner point if view, we are effective owner as we received "activeStack" HypotheticBattle hb(getCbc()); PotentialTargets targets(stack, &hb); if(targets.possibleAttacks.size()) { auto hlp = targets.bestAction(); if(hlp.attack.shooting) return BattleAction::makeShotAttack(stack, hlp.attack.defender); else return BattleAction::makeMeleeAttack(stack, hlp.attack.defender->getPosition(), hlp.tile); } else { if(stack->waited()) { //ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code. auto dists = getCbc()->battleGetDistances(stack, stack->getPosition()); if(!targets.unreachableEnemies.empty()) { const EnemyInfo &ei= *range::min_element(targets.unreachableEnemies, std::bind(isCloser, _1, _2, std::ref(dists))); if(distToNearestNeighbour(ei.s->getPosition(), dists) < GameConstants::BFIELD_SIZE) { return goTowards(stack, ei.s->getPosition()); } } } else { return BattleAction::makeWait(stack); } } } catch(boost::thread_interrupted &) { throw; } catch(std::exception &e) { logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what()); } return BattleAction::makeDefend(stack); } BattleAction CBattleAI::goTowards(const CStack * stack, BattleHex destination) { if(!destination.isValid()) { logAi->error("CBattleAI::goTowards: invalid destination"); return BattleAction::makeDefend(stack); } auto reachability = cb->getReachability(stack); auto avHexes = cb->battleGetAvailableHexes(reachability, stack); if(vstd::contains(avHexes, destination)) return BattleAction::makeMove(stack, destination); auto destNeighbours = destination.neighbouringTiles(); if(vstd::contains_if(destNeighbours, [&](BattleHex n) { return stack->coversPos(destination); })) { logAi->warn("Warning: already standing on neighbouring tile!"); //We shouldn't even be here... return BattleAction::makeDefend(stack); } vstd::erase_if(destNeighbours, [&](BattleHex hex){ return !reachability.accessibility.accessible(hex, stack); }); if(!avHexes.size() || !destNeighbours.size()) //we are blocked or dest is blocked { return BattleAction::makeDefend(stack); } if(stack->hasBonusOfType(Bonus::FLYING)) { // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. // We just check all available hexes and pick the one closest to the target. auto distToDestNeighbour = [&](BattleHex hex) -> int { auto nearestNeighbourToHex = vstd::minElementByFun(destNeighbours, [&](BattleHex a) {return BattleHex::getDistance(a, hex);}); return BattleHex::getDistance(*nearestNeighbourToHex, hex); }; auto nearestAvailableHex = vstd::minElementByFun(avHexes, distToDestNeighbour); return BattleAction::makeMove(stack, *nearestAvailableHex); } else { BattleHex bestNeighbor = destination; if(distToNearestNeighbour(destination, reachability.distances, &bestNeighbor) > GameConstants::BFIELD_SIZE) { return BattleAction::makeDefend(stack); } BattleHex currentDest = bestNeighbor; while(1) { if(!currentDest.isValid()) { logAi->error("CBattleAI::goTowards: internal error"); return BattleAction::makeDefend(stack); } if(vstd::contains(avHexes, currentDest)) return BattleAction::makeMove(stack, currentDest); currentDest = reachability.predecessors[currentDest]; } } } BattleAction CBattleAI::useCatapult(const CStack * stack) { throw std::runtime_error("CBattleAI::useCatapult is not implemented."); } void CBattleAI::attemptCastingSpell() { auto hero = cb->battleGetMyHero(); if(!hero) return; if(cb->battleCanCastSpell(hero, spells::Mode::HERO) != ESpellCastProblem::OK) return; LOGL("Casting spells sounds like fun. Let's see..."); //Get all spells we can cast std::vector possibleSpells; vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero](const CSpell *s) -> bool { return s->canBeCast(getCbc().get(), spells::Mode::HERO, hero); }); LOGFL("I can cast %d spells.", possibleSpells.size()); vstd::erase_if(possibleSpells, [](const CSpell *s) { return spellType(s) != SpellTypes::BATTLE; }); LOGFL("I know how %d of them works.", possibleSpells.size()); //Get possible spell-target pairs std::vector possibleCasts; for(auto spell : possibleSpells) { spells::BattleCast temp(getCbc().get(), hero, spells::Mode::HERO, spell); for(auto & target : temp.findPotentialTargets()) { PossibleSpellcast ps; ps.dest = target; ps.spell = spell; possibleCasts.push_back(ps); } } LOGFL("Found %d spell-target combinations.", possibleCasts.size()); if(possibleCasts.empty()) return; using ValueMap = PossibleSpellcast::ValueMap; auto evaluateQueue = [&](ValueMap & values, const std::vector & queue, HypotheticBattle * state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool { bool firstRound = true; bool enemyHadTurn = false; size_t ourTurnSpan = 0; bool stop = false; for(auto & round : queue) { if(!firstRound) state->nextRound(0);//todo: set actual value? for(auto unit : round) { if(!vstd::contains(values, unit->unitId())) values[unit->unitId()] = 0; if(!unit->alive()) continue; if(state->battleGetOwner(unit) != playerID) { enemyHadTurn = true; if(!firstRound || state->battleCastSpells(unit->unitSide()) == 0) { //enemy could counter our spell at this point //anyway, we do not know what enemy will do //just stop evaluation stop = true; break; } } else if(!enemyHadTurn) { ourTurnSpan++; } state->nextTurn(unit->unitId()); PotentialTargets pt(unit, state); if(!pt.possibleAttacks.empty()) { AttackPossibility ap = pt.bestAction(); auto swb = state->getForUpdate(unit->unitId()); *swb = *ap.attackerState; if(ap.damageDealt > 0) swb->removeUnitBonus(Bonus::UntilAttack); if(ap.damageReceived > 0) swb->removeUnitBonus(Bonus::UntilBeingAttacked); for(auto affected : ap.affectedUnits) { swb = state->getForUpdate(affected->unitId()); *swb = *affected; if(ap.damageDealt > 0) swb->removeUnitBonus(Bonus::UntilBeingAttacked); if(ap.damageReceived > 0 && ap.attack.defender->unitId() == affected->unitId()) swb->removeUnitBonus(Bonus::UntilAttack); } } auto bav = pt.bestActionValue(); //best action is from effective owner`s point if view, we need to convert to our point if view if(state->battleGetOwner(unit) != playerID) bav = -bav; values[unit->unitId()] += bav; } firstRound = false; if(stop) break; } if(enemyHadTurnOut) *enemyHadTurnOut = enemyHadTurn; return ourTurnSpan >= minTurnSpan; }; RNGStub rngStub; ValueMap valueOfStack; ValueMap healthOfStack; TStacks all = cb->battleGetAllStacks(false); size_t ourRemainingTurns = 0; for(auto unit : all) { healthOfStack[unit->unitId()] = unit->getAvailableHealth(); valueOfStack[unit->unitId()] = 0; if(cb->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved()) ourRemainingTurns++; } LOGFL("I have %d turns left in this round", ourRemainingTurns); const bool castNow = ourRemainingTurns <= 1; if(castNow) print("I should try to cast a spell now"); else print("I could wait better moment to cast a spell"); auto amount = all.size(); std::vector turnOrder; cb->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once { bool enemyHadTurn = false; HypotheticBattle state(cb); evaluateQueue(valueOfStack, turnOrder, &state, 0, &enemyHadTurn); if(!enemyHadTurn) { auto battleIsFinishedOpt = state.battleIsFinished(); if(battleIsFinishedOpt) { print("No need to cast a spell. Battle will finish soon."); return; } } } auto evaluateSpellcast = [&] (PossibleSpellcast * ps) { HypotheticBattle state(cb); spells::BattleCast cast(&state, hero, spells::Mode::HERO, ps->spell); cast.target = ps->dest; cast.cast(&state, rngStub); ValueMap newHealthOfStack; ValueMap newValueOfStack; size_t ourUnits = 0; for(auto unit : all) { auto unitId = unit->unitId(); auto localUnit = state.battleGetUnitByID(unitId); newHealthOfStack[unitId] = localUnit->getAvailableHealth(); newValueOfStack[unitId] = 0; if(state.battleGetOwner(localUnit) == playerID && localUnit->alive() && localUnit->willMove()) ourUnits++; } size_t minTurnSpan = ourUnits/3; //todo: tweak this std::vector newTurnOrder; state.battleGetTurnOrder(newTurnOrder, amount, 2); const bool turnSpanOK = evaluateQueue(newValueOfStack, newTurnOrder, &state, minTurnSpan, nullptr); if(turnSpanOK || castNow) { int64_t totalGain = 0; for(auto unit : all) { auto unitId = unit->unitId(); auto localUnit = state.battleGetUnitByID(unitId); auto newValue = getValOr(newValueOfStack, unitId, 0); auto oldValue = getValOr(valueOfStack, unitId, 0); auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId]; if(localUnit->unitOwner() != playerID) healthDiff = -healthDiff; if(healthDiff < 0) { ps->value = -1; return; //do not damage own units at all } totalGain += (newValue - oldValue + healthDiff); } ps->value = totalGain; } else { ps->value = -1; } }; std::vector> tasks; for(PossibleSpellcast & psc : possibleCasts) tasks.push_back(std::bind(evaluateSpellcast, &psc)); uint32_t threadCount = boost::thread::hardware_concurrency(); if(threadCount == 0) { logGlobal->warn("No information of CPU cores available"); threadCount = 1; } CStopWatch timer; CThreadHelper threadHelper(&tasks, threadCount); threadHelper.run(); LOGFL("Evaluation took %d ms", timer.getDiff()); auto pscValue = [](const PossibleSpellcast &ps) -> int64_t { return ps.value; }; auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); if(castToPerform.value > 0) { LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->name % castToPerform.value); BattleAction spellcast; spellcast.actionType = EActionType::HERO_SPELL; spellcast.actionSubtype = castToPerform.spell->id; spellcast.setTarget(castToPerform.dest); spellcast.side = side; spellcast.stackNumber = (!side) ? -1 : -2; cb->battleMakeAction(&spellcast); } else { LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->name % castToPerform.value); } } int CBattleAI::distToNearestNeighbour(BattleHex hex, const ReachabilityInfo::TDistances &dists, BattleHex *chosenHex) { int ret = 1000000; for(BattleHex n : hex.neighbouringTiles()) { if(dists[n] >= 0 && dists[n] < ret) { ret = dists[n]; if(chosenHex) *chosenHex = n; } } return ret; } void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side) { LOG_TRACE(logAi); side = Side; } bool CBattleAI::isCloser(const EnemyInfo &ei1, const EnemyInfo &ei2, const ReachabilityInfo::TDistances &dists) { return distToNearestNeighbour(ei1.s->getPosition(), dists) < distToNearestNeighbour(ei2.s->getPosition(), dists); } void CBattleAI::print(const std::string &text) const { logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); } boost::optional CBattleAI::considerFleeingOrSurrendering() { if(cb->battleCanSurrender(playerID)) { } if(cb->battleCanFlee()) { } return boost::none; } vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/BattleAI.h000066400000000000000000000072061342332007200206730ustar00rootroot00000000000000/* * BattleAI.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/AI_Base.h" #include "PotentialTargets.h" class CSpell; class EnemyInfo; /* struct CurrentOffensivePotential { std::map ourAttacks; std::map enemyAttacks; CurrentOffensivePotential(ui8 side) { for(auto stack : cbc->battleGetStacks()) { if(stack->side == side) ourAttacks[stack] = PotentialTargets(stack); else enemyAttacks[stack] = PotentialTargets(stack); } } int potentialValue() { int ourPotential = 0, enemyPotential = 0; for(auto &p : ourAttacks) ourPotential += p.second.bestAction().attackValue(); for(auto &p : enemyAttacks) enemyPotential += p.second.bestAction().attackValue(); return ourPotential - enemyPotential; } }; */ // These lines may be usefull but they are't used in the code. class CBattleAI : public CBattleGameInterface { int side; std::shared_ptr cb; //Previous setting of cb bool wasWaitingForRealize, wasUnlockingGs; public: CBattleAI(); ~CBattleAI(); void init(std::shared_ptr CB) override; void attemptCastingSpell(); BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack BattleAction goTowards(const CStack * stack, BattleHex hex ); boost::optional considerFleeingOrSurrendering(); static int distToNearestNeighbour(BattleHex hex, const ReachabilityInfo::TDistances& dists, BattleHex *chosenHex = nullptr); static bool isCloser(const EnemyInfo & ei1, const EnemyInfo & ei2, const ReachabilityInfo::TDistances & dists); void print(const std::string &text) const; BattleAction useCatapult(const CStack *stack); void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side) override; //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero //void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero //void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack //void battleStacksAttacked(const std::vector & bsa, const std::vector & battleLog) override; //called when stack receives damage (after battleAttack()) //void battleEnd(const BattleResult *br) override; //void battleResultsApplied() override; //called when all effects of last battle are applied //void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; //void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn //void battleStackMoved(const CStack * stack, std::vector dest, int distance) override; //void battleSpellCast(const BattleSpellCast *sc) override; //void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks //void battleTriggerEffect(const BattleTriggerEffect & bte) override; //void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right //void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack }; vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/BattleAI.vcxproj000066400000000000000000000207401342332007200221350ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 {C0300513-E845-43B4-9A4F-E8817EAEF57C} BattleAI 10.0.17763.0 DynamicLibrary true MultiByte v140_xp DynamicLibrary true MultiByte v140_xp DynamicLibrary false true MultiByte v141 DynamicLibrary false true MultiByte v140_xp $(VCMI_Out)\AI\ $(VCMI_Out)\AI\ $(VCMI_Out)/AI $(VCMI_Out)\AI\ Use StdInc.h /Zm159 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) ..\..\..\libs;..\..;.. Level3 Disabled Use StdInc.h /Zm159 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) Use StdInc.h true VCMI_lib.lib;%(AdditionalDependencies) $(VCMI_Out) Use StdInc.h /Zm159 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) %(PreprocessorDefinitions) Create StdInc.h Create Create Create vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/CMakeLists.txt000066400000000000000000000020141342332007200216250ustar00rootroot00000000000000include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_HOME_DIRECTORY}/include ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib) set(battleAI_SRCS StdInc.cpp AttackPossibility.cpp BattleAI.cpp common.cpp EnemyInfo.cpp main.cpp PossibleSpellcast.cpp PotentialTargets.cpp StackWithBonuses.cpp ThreatMap.cpp ) set(battleAI_HEADERS StdInc.h AttackPossibility.h BattleAI.h common.h EnemyInfo.h PotentialTargets.h PossibleSpellcast.h StackWithBonuses.h ThreatMap.h ) assign_source_group(${battleAI_SRCS} ${battleAI_HEADERS}) if(ANDROID) # android compiles ai libs into main lib directly, so we skip this library and just reuse sources list return() endif() add_library(BattleAI SHARED ${battleAI_SRCS} ${battleAI_HEADERS}) target_link_libraries(BattleAI vcmi) vcmi_set_output_dir(BattleAI "AI") set_target_properties(BattleAI PROPERTIES ${PCH_PROPERTIES}) cotire(BattleAI) install(TARGETS BattleAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR}) vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/EnemyInfo.cpp000066400000000000000000000006261342332007200214710ustar00rootroot00000000000000/* * EnemyInfo.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "EnemyInfo.h" #include "../../lib/battle/Unit.h" bool EnemyInfo::operator==(const EnemyInfo & ei) const { return s->unitId() == ei.s->unitId(); } vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/EnemyInfo.h000066400000000000000000000006461342332007200211400ustar00rootroot00000000000000/* * EnemyInfo.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once namespace battle { class Unit; } class EnemyInfo { public: const battle::Unit * s; EnemyInfo(const battle::Unit * _s) : s(_s) {} bool operator==(const EnemyInfo & ei) const; }; vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/PossibleSpellcast.cpp000066400000000000000000000006451342332007200232340ustar00rootroot00000000000000/* * PossibleSpellcast.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "PossibleSpellcast.h" PossibleSpellcast::PossibleSpellcast() : spell(nullptr), dest(), value(0) { } PossibleSpellcast::~PossibleSpellcast() = default; vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/PossibleSpellcast.h000066400000000000000000000010361342332007200226740ustar00rootroot00000000000000/* * PossibleSpellcast.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/battle/Destination.h" #include "../../lib/spells/Magic.h" class CSpell; class PossibleSpellcast { public: using ValueMap = std::map; const CSpell * spell; spells::Target dest; int64_t value; PossibleSpellcast(); virtual ~PossibleSpellcast(); }; vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/PotentialTargets.cpp000066400000000000000000000054661342332007200231000ustar00rootroot00000000000000/* * PotentialTargets.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "PotentialTargets.h" #include "../../lib/CStack.h"//todo: remove PotentialTargets::PotentialTargets(const battle::Unit * attacker, const HypotheticBattle * state) { auto attIter = state->stackStates.find(attacker->unitId()); const battle::Unit * attackerInfo = (attIter == state->stackStates.end()) ? attacker : attIter->second.get(); auto reachability = state->getReachability(attackerInfo); auto avHexes = state->battleGetAvailableHexes(reachability, attackerInfo); //FIXME: this should part of battleGetAvailableHexes bool forceTarget = false; const battle::Unit * forcedTarget = nullptr; BattleHex forcedHex; if(attackerInfo->hasBonusOfType(Bonus::ATTACKS_NEAREST_CREATURE)) { forceTarget = true; auto nearest = state->getNearestStack(attackerInfo); if(nearest.first != nullptr) { forcedTarget = nearest.first; forcedHex = nearest.second; } } auto aliveUnits = state->battleGetUnitsIf([=](const battle::Unit * unit) { return unit->isValidTarget() && unit->unitId() != attackerInfo->unitId(); }); for(auto defender : aliveUnits) { if(!forceTarget && !state->battleMatchOwner(attackerInfo, defender)) continue; auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility { auto bai = BattleAttackInfo(attackerInfo, defender, shooting); if(hex.isValid() && !shooting) bai.chargedFields = reachability.distances[hex]; return AttackPossibility::evaluate(bai, hex); }; if(forceTarget) { if(forcedTarget && defender->unitId() == forcedTarget->unitId()) possibleAttacks.push_back(GenerateAttackInfo(false, forcedHex)); else unreachableEnemies.push_back(defender); } else if(state->battleCanShoot(attackerInfo, defender->getPosition())) { possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID)); } else { for(BattleHex hex : avHexes) if(CStack::isMeleeAttackPossible(attackerInfo, defender, hex)) possibleAttacks.push_back(GenerateAttackInfo(false, hex)); if(!vstd::contains_if(possibleAttacks, [=](const AttackPossibility & pa) { return pa.attack.defender->unitId() == defender->unitId(); })) unreachableEnemies.push_back(defender); } } } int PotentialTargets::bestActionValue() const { if(possibleAttacks.empty()) return 0; return bestAction().attackValue(); } AttackPossibility PotentialTargets::bestAction() const { if(possibleAttacks.empty()) throw std::runtime_error("No best action, since we don't have any actions"); return *vstd::maxElementByFun(possibleAttacks, [](const AttackPossibility &ap) { return ap.attackValue(); } ); } vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/PotentialTargets.h000066400000000000000000000011211342332007200225250ustar00rootroot00000000000000/* * PotentialTargets.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "AttackPossibility.h" class PotentialTargets { public: std::vector possibleAttacks; std::vector unreachableEnemies; PotentialTargets(){}; PotentialTargets(const battle::Unit * attacker, const HypotheticBattle * state); AttackPossibility bestAction() const; int bestActionValue() const; }; vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/StackWithBonuses.cpp000066400000000000000000000212351342332007200230370ustar00rootroot00000000000000/* * StackWithBonuses.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "StackWithBonuses.h" #include "../../lib/NetPacksBase.h" #include "../../lib/CStack.h" void actualizeEffect(TBonusListPtr target, const Bonus & ef) { for(auto bonus : *target) //TODO: optimize { if(bonus->source == Bonus::SPELL_EFFECT && bonus->type == ef.type && bonus->subtype == ef.subtype) { bonus->turnsRemain = std::max(bonus->turnsRemain, ef.turnsRemain); } } } StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const CStack * Stack) : battle::CUnitState(), origBearer(Stack), owner(Owner), type(Stack->unitType()), baseAmount(Stack->unitBaseAmount()), id(Stack->unitId()), side(Stack->unitSide()), player(Stack->unitOwner()), slot(Stack->unitSlot()) { localInit(Owner); battle::CUnitState::operator=(*Stack); } StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info) : battle::CUnitState(), origBearer(nullptr), owner(Owner), baseAmount(info.count), id(info.id), side(info.side), slot(SlotID::SUMMONED_SLOT_PLACEHOLDER) { type = info.type.toCreature(); origBearer = type; player = Owner->getSidePlayer(side); localInit(Owner); position = info.position; summoned = info.summoned; } StackWithBonuses::~StackWithBonuses() = default; StackWithBonuses & StackWithBonuses::operator=(const battle::CUnitState & other) { battle::CUnitState::operator=(other); return *this; } const CCreature * StackWithBonuses::unitType() const { return type; } int32_t StackWithBonuses::unitBaseAmount() const { return baseAmount; } uint32_t StackWithBonuses::unitId() const { return id; } ui8 StackWithBonuses::unitSide() const { return side; } PlayerColor StackWithBonuses::unitOwner() const { return player; } SlotID StackWithBonuses::unitSlot() const { return slot; } const TBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, const CSelector & limit, const CBonusSystemNode * root, const std::string & cachingStr) const { TBonusListPtr ret = std::make_shared(); const TBonusListPtr originalList = origBearer->getAllBonuses(selector, limit, root, cachingStr); vstd::copy_if(*originalList, std::back_inserter(*ret), [this](const std::shared_ptr & b) { return !vstd::contains(bonusesToRemove, b); }); for(const Bonus & bonus : bonusesToUpdate) { if(selector(&bonus) && (!limit || !limit(&bonus))) { if(ret->getFirst(Selector::source(Bonus::SPELL_EFFECT, bonus.sid).And(Selector::typeSubtype(bonus.type, bonus.subtype)))) { actualizeEffect(ret, bonus); } else { auto b = std::make_shared(bonus); ret->push_back(b); } } } for(auto & bonus : bonusesToAdd) { auto b = std::make_shared(bonus); if(selector(b.get()) && (!limit || !limit(b.get()))) ret->push_back(b); } //TODO limiters? return ret; } int64_t StackWithBonuses::getTreeVersion() const { return owner->getTreeVersion(); } void StackWithBonuses::addUnitBonus(const std::vector & bonus) { vstd::concatenate(bonusesToAdd, bonus); } void StackWithBonuses::updateUnitBonus(const std::vector & bonus) { //TODO: optimize, actualize to last value vstd::concatenate(bonusesToUpdate, bonus); } void StackWithBonuses::removeUnitBonus(const std::vector & bonus) { for(auto & one : bonus) { CSelector selector([&one](const Bonus * b) -> bool { //compare everything but turnsRemain, limiter and propagator return one.duration == b->duration && one.type == b->type && one.subtype == b->subtype && one.source == b->source && one.val == b->val && one.sid == b->sid && one.valType == b->valType && one.additionalInfo == b->additionalInfo && one.effectRange == b->effectRange && one.description == b->description; }); removeUnitBonus(selector); } } void StackWithBonuses::removeUnitBonus(const CSelector & selector) { TBonusListPtr toRemove = origBearer->getBonuses(selector); for(auto b : *toRemove) bonusesToRemove.insert(b); vstd::erase_if(bonusesToAdd, [&](const Bonus & b){return selector(&b);}); vstd::erase_if(bonusesToUpdate, [&](const Bonus & b){return selector(&b);}); } void StackWithBonuses::spendMana(const spells::PacketSender * server, const int spellCost) const { //TODO: evaluate cast use } HypotheticBattle::HypotheticBattle(Subject realBattle) : BattleProxy(realBattle), bonusTreeVersion(1) { auto activeUnit = realBattle->battleActiveUnit(); activeUnitId = activeUnit ? activeUnit->unitId() : -1; nextId = 0xF0000000; } bool HypotheticBattle::unitHasAmmoCart(const battle::Unit * unit) const { //FIXME: check ammocart alive state here return false; } PlayerColor HypotheticBattle::unitEffectiveOwner(const battle::Unit * unit) const { return battleGetOwner(unit); } std::shared_ptr HypotheticBattle::getForUpdate(uint32_t id) { auto iter = stackStates.find(id); if(iter == stackStates.end()) { const CStack * s = subject->battleGetStackByID(id, false); auto ret = std::make_shared(this, s); stackStates[id] = ret; return ret; } else { return iter->second; } } battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const { battle::Units proxyed = BattleProxy::getUnitsIf(predicate); battle::Units ret; ret.reserve(proxyed.size()); for(auto unit : proxyed) { //unit was not changed, trust proxyed data if(stackStates.find(unit->unitId()) == stackStates.end()) ret.push_back(unit); } for(auto id_unit : stackStates) { if(predicate(id_unit.second.get())) ret.push_back(id_unit.second.get()); } return ret; } int32_t HypotheticBattle::getActiveStackID() const { return activeUnitId; } void HypotheticBattle::nextRound(int32_t roundNr) { //TODO:HypotheticBattle::nextRound for(auto unit : battleAliveUnits()) { auto forUpdate = getForUpdate(unit->unitId()); //TODO: update Bonus::NTurns effects forUpdate->afterNewRound(); } } void HypotheticBattle::nextTurn(uint32_t unitId) { activeUnitId = unitId; auto unit = getForUpdate(unitId); unit->removeUnitBonus(Bonus::UntilGetsTurn); unit->afterGetsTurn(); } void HypotheticBattle::addUnit(uint32_t id, const JsonNode & data) { battle::UnitInfo info; info.load(id, data); std::shared_ptr newUnit = std::make_shared(this, info); stackStates[newUnit->unitId()] = newUnit; } void HypotheticBattle::moveUnit(uint32_t id, BattleHex destination) { std::shared_ptr changed = getForUpdate(id); changed->position = destination; } void HypotheticBattle::setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) { std::shared_ptr changed = getForUpdate(id); changed->load(data); if(healthDelta < 0) { changed->removeUnitBonus(Bonus::UntilBeingAttacked); } } void HypotheticBattle::removeUnit(uint32_t id) { std::set ids; ids.insert(id); while(!ids.empty()) { auto toRemoveId = *ids.begin(); auto toRemove = getForUpdate(toRemoveId); if(!toRemove->ghost) { toRemove->onRemoved(); //TODO: emulate detachFromAll() somehow //stack may be removed instantly (not being killed first) //handle clone remove also here if(toRemove->cloneID >= 0) { ids.insert(toRemove->cloneID); toRemove->cloneID = -1; } //TODO: cleanup remaining clone links if any // for(auto s : stacks) // { // if(s->cloneID == toRemoveId) // s->cloneID = -1; // } } ids.erase(toRemoveId); } } void HypotheticBattle::addUnitBonus(uint32_t id, const std::vector & bonus) { getForUpdate(id)->addUnitBonus(bonus); bonusTreeVersion++; } void HypotheticBattle::updateUnitBonus(uint32_t id, const std::vector & bonus) { getForUpdate(id)->updateUnitBonus(bonus); bonusTreeVersion++; } void HypotheticBattle::removeUnitBonus(uint32_t id, const std::vector & bonus) { getForUpdate(id)->removeUnitBonus(bonus); bonusTreeVersion++; } void HypotheticBattle::setWallState(int partOfWall, si8 state) { //TODO:HypotheticBattle::setWallState } void HypotheticBattle::addObstacle(const ObstacleChanges & changes) { //TODO:HypotheticBattle::addObstacle } void HypotheticBattle::removeObstacle(uint32_t id) { //TODO:HypotheticBattle::removeObstacle } uint32_t HypotheticBattle::nextUnitId() const { return nextId++; } int64_t HypotheticBattle::getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const { return (damage.first + damage.second) / 2; } int64_t HypotheticBattle::getTreeVersion() const { return getBattleNode()->getTreeVersion() + bonusTreeVersion; } vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/StackWithBonuses.h000066400000000000000000000064631342332007200225120ustar00rootroot00000000000000/* * StackWithBonuses.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/HeroBonus.h" #include "../../lib/battle/BattleProxy.h" #include "../../lib/battle/CUnitState.h" class HypotheticBattle; class CStack; class StackWithBonuses : public battle::CUnitState, public virtual IBonusBearer { public: std::vector bonusesToAdd; std::vector bonusesToUpdate; std::set> bonusesToRemove; StackWithBonuses(const HypotheticBattle * Owner, const CStack * Stack); StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info); virtual ~StackWithBonuses(); StackWithBonuses & operator= (const battle::CUnitState & other); ///IUnitInfo const CCreature * unitType() const override; int32_t unitBaseAmount() const override; uint32_t unitId() const override; ui8 unitSide() const override; PlayerColor unitOwner() const override; SlotID unitSlot() const override; ///IBonusBearer const TBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, const CBonusSystemNode * root = nullptr, const std::string & cachingStr = "") const override; int64_t getTreeVersion() const override; void addUnitBonus(const std::vector & bonus); void updateUnitBonus(const std::vector & bonus); void removeUnitBonus(const std::vector & bonus); void removeUnitBonus(const CSelector & selector); void spendMana(const spells::PacketSender * server, const int spellCost) const override; private: const IBonusBearer * origBearer; const HypotheticBattle * owner; const CCreature * type; ui32 baseAmount; uint32_t id; ui8 side; PlayerColor player; SlotID slot; }; class HypotheticBattle : public BattleProxy, public battle::IUnitEnvironment { public: std::map> stackStates; HypotheticBattle(Subject realBattle); bool unitHasAmmoCart(const battle::Unit * unit) const override; PlayerColor unitEffectiveOwner(const battle::Unit * unit) const override; std::shared_ptr getForUpdate(uint32_t id); int32_t getActiveStackID() const override; battle::Units getUnitsIf(battle::UnitFilter predicate) const override; void nextRound(int32_t roundNr) override; void nextTurn(uint32_t unitId) override; void addUnit(uint32_t id, const JsonNode & data) override; void setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) override; void moveUnit(uint32_t id, BattleHex destination) override; void removeUnit(uint32_t id) override; void addUnitBonus(uint32_t id, const std::vector & bonus) override; void updateUnitBonus(uint32_t id, const std::vector & bonus) override; void removeUnitBonus(uint32_t id, const std::vector & bonus) override; void setWallState(int partOfWall, si8 state) override; void addObstacle(const ObstacleChanges & changes) override; void removeObstacle(uint32_t id) override; uint32_t nextUnitId() const override; int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; int64_t getTreeVersion() const; private: int32_t bonusTreeVersion; int32_t activeUnitId; mutable uint32_t nextId; }; vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/StdInc.cpp000066400000000000000000000004411342332007200207570ustar00rootroot00000000000000/* * StdInc.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ // Creates the precompiled header #include "StdInc.h" vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/StdInc.h000066400000000000000000000007231342332007200204270ustar00rootroot00000000000000/* * StdInc.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../Global.h" // This header should be treated as a pre compiled header file(PCH) in the compiler building settings. // Here you can add specific libraries and macros which are specific to this project. vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/ThreatMap.cpp000066400000000000000000000036651342332007200214730ustar00rootroot00000000000000/* * ThreatMap.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" /* #include "ThreatMap.h" #include "StdInc.h" template auto sum(const Container & c, Pred p) -> decltype(p(*std::begin(c))) { double ret = 0; for(const auto &element : c) { ret += p(element); } return ret; } ThreatMap::ThreatMap(const CStack *Endangered) : endangered(Endangered) { sufferedDamage.fill(0); for(const CStack *enemy : getCbc()->battleGetStacks()) { //Consider only stacks of different owner if(enemy->side == endangered->side) continue; //Look-up which tiles can be melee-attacked std::array meleeAttackable; meleeAttackable.fill(false); auto enemyReachability = getCbc()->getReachability(enemy); for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) { if(enemyReachability.isReachable(i)) { meleeAttackable[i] = true; for(auto n : BattleHex(i).neighbouringTiles()) meleeAttackable[n] = true; } } //Gather possible assaults for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) { if(getCbc()->battleCanShoot(enemy, i)) threatMap[i].push_back(BattleAttackInfo(enemy, endangered, true)); else if(meleeAttackable[i]) { BattleAttackInfo bai(enemy, endangered, false); bai.chargedFields = std::max(BattleHex::getDistance(enemy->position, i) - 1, 0); //TODO check real distance (BFS), not just metric threatMap[i].push_back(BattleAttackInfo(bai)); } } } for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) { sufferedDamage[i] = sum(threatMap[i], [](const BattleAttackInfo &bai) -> int { auto dmg = getCbc()->calculateDmgRange(bai); return (dmg.first + dmg.second)/2; }); } } */ // These lines may be usefull but they are't used in the code. vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/ThreatMap.h000066400000000000000000000011651342332007200211310ustar00rootroot00000000000000/* * ThreatMap.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ /* #pragma once #include "common.h" #include "CCallback.h" /* class ThreatMap { public: std::array, GameConstants::BFIELD_SIZE> threatMap; // [hexNr] -> enemies able to strike const CStack *endangered; std::array sufferedDamage; ThreatMap(const CStack *Endangered); };*/ // These lines may be usefull but they are't used in the code. vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/common.cpp000066400000000000000000000006541342332007200210710ustar00rootroot00000000000000/* * common.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "common.h" std::shared_ptr cbc; void setCbc(std::shared_ptr cb) { cbc = cb; } std::shared_ptr getCbc() { return cbc; } vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/common.h000066400000000000000000000012701342332007200205310ustar00rootroot00000000000000/* * common.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once class CBattleCallback; template const Val getValOr(const std::map &Map, const Key &key, const Val2 defaultValue) { //returning references here won't work: defaultValue must be converted into Val, creating temporary auto i = Map.find(key); if(i != Map.end()) return i->second; else return defaultValue; } void setCbc(std::shared_ptr cb); std::shared_ptr getCbc(); vcmi-0.99+git20190113.f06c8a87/AI/BattleAI/main.cpp000066400000000000000000000013661342332007200205260ustar00rootroot00000000000000/* * main.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "../../lib/AI_Base.h" #include "BattleAI.h" #ifdef __GNUC__ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif static const char *g_cszAiName = "Battle AI"; extern "C" DLL_EXPORT int GetGlobalAiVersion() { return AI_INTERFACE_VER; } extern "C" DLL_EXPORT void GetAiName(char* name) { strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); } extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) { out = std::make_shared(); } vcmi-0.99+git20190113.f06c8a87/AI/CMakeLists.txt000066400000000000000000000015321342332007200202040ustar00rootroot00000000000000####################################### # FuzzyLite support # ####################################### if(NOT WIN32 AND NOT APPLE) option(FORCE_BUNDLED_FL "Force to use FuzzyLite included into VCMI's source tree" ON) else() option(FORCE_BUNDLED_FL "Force to use FuzzyLite included into VCMI's source tree" OFF) endif() if(NOT FORCE_BUNDLED_FL) find_package(FuzzyLite) else() set(FL_FOUND FALSE) endif() if(NOT FL_FOUND) set(FL_BUILD_BINARY OFF CACHE BOOL "") set(FL_BUILD_SHARED OFF CACHE BOOL "") set(FL_BUILD_TESTS OFF CACHE BOOL "") add_subdirectory(FuzzyLite/fuzzylite EXCLUDE_FROM_ALL) endif() ####################################### # Add subdirectories # ####################################### add_subdirectory(BattleAI) add_subdirectory(StupidAI) add_subdirectory(EmptyAI) add_subdirectory(VCAI) vcmi-0.99+git20190113.f06c8a87/AI/EmptyAI/000077500000000000000000000000001342332007200167535ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/AI/EmptyAI/CEmptyAI.cpp000066400000000000000000000032451342332007200210760ustar00rootroot00000000000000/* * CEmptyAI.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CEmptyAI.h" #include "../../lib/CRandomGenerator.h" void CEmptyAI::init(std::shared_ptr CB) { cb = CB; human=false; playerID = *cb->getMyColor(); } void CEmptyAI::yourTurn() { cb->endTurn(); } void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) { cb->selectionMade(CRandomGenerator::getDefault().nextInt(skills.size() - 1), queryID); } void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) { cb->selectionMade(CRandomGenerator::getDefault().nextInt(skills.size() - 1), queryID); } void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) { cb->selectionMade(0, askID); } void CEmptyAI::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { cb->selectionMade(0, askID); } void CEmptyAI::showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) { cb->selectionMade(0, queryID); } void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) { cb->selectionMade(0, askID); } vcmi-0.99+git20190113.f06c8a87/AI/EmptyAI/CEmptyAI.h000066400000000000000000000025731342332007200205460ustar00rootroot00000000000000/* * CEmptyAI.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/AI_Base.h" #include "../../CCallback.h" struct HeroMoveDetails; class CEmptyAI : public CGlobalAI { std::shared_ptr cb; public: void init(std::shared_ptr CB) override; void yourTurn() override; void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) override; void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; }; #define NAME "EmptyAI 0.1" vcmi-0.99+git20190113.f06c8a87/AI/EmptyAI/CMakeLists.txt000066400000000000000000000011471342332007200215160ustar00rootroot00000000000000include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_HOME_DIRECTORY}/include ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib) set(emptyAI_SRCS StdInc.cpp CEmptyAI.cpp exp_funcs.cpp ) set(emptyAI_HEADERS StdInc.h CEmptyAI.h ) assign_source_group(${emptyAI_SRCS} ${emptyAI_HEADERS}) add_library(EmptyAI SHARED ${emptyAI_SRCS} ${emptyAI_HEADERS}) target_link_libraries(EmptyAI vcmi) vcmi_set_output_dir(EmptyAI "AI") set_target_properties(EmptyAI PROPERTIES ${PCH_PROPERTIES}) install(TARGETS EmptyAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR}) vcmi-0.99+git20190113.f06c8a87/AI/EmptyAI/EmptyAI.cbp000066400000000000000000000053041342332007200207530ustar00rootroot00000000000000 vcmi-0.99+git20190113.f06c8a87/AI/EmptyAI/EmptyAI.vcxproj000066400000000000000000000227171342332007200217110ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 Create Create Create Create {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837} EmptyAI 10.0.17763.0 DynamicLibrary true MultiByte v140_xp DynamicLibrary true MultiByte v140_xp DynamicLibrary false true MultiByte v141 DynamicLibrary false true MultiByte v140_xp $(VCMI_Out)\AI\ $(IncludePath) $(LibraryPath) $(VCMI_Out)\AI\ $(VCMI_Out)/AI $(VCMI_Out)\AI\ Level3 Disabled %(AdditionalIncludeDirectories) Use StdInc.h %(PreprocessorDefinitions) /Zm130 %(AdditionalOptions) true VCMI_lib.lib;%(AdditionalDependencies) $(OutDir)EmptyAI.dll ..\..\..\libs;..\.. Level3 Disabled %(AdditionalIncludeDirectories) Use StdInc.h _WINDLL;%(PreprocessorDefinitions) true VCMI_lib.lib;%(AdditionalDependencies) $(OutDir)EmptyAI.dll Level3 MaxSpeed true true %(AdditionalIncludeDirectories) Use StdInc.h _WINDLL;%(PreprocessorDefinitions) true true true true VCMI_lib.lib;%(AdditionalDependencies) $(OutDir)EmptyAI.dll $(VCMI_Out) Level3 MaxSpeed true true %(AdditionalIncludeDirectories) Use StdInc.h _WINDLL;%(PreprocessorDefinitions) /Zm130 %(AdditionalOptions) true true true VCMI_lib.lib;%(AdditionalDependencies) $(OutDir)EmptyAI.dll vcmi-0.99+git20190113.f06c8a87/AI/EmptyAI/StdInc.cpp000066400000000000000000000000671342332007200206460ustar00rootroot00000000000000// Creates the precompiled header #include "StdInc.h" vcmi-0.99+git20190113.f06c8a87/AI/EmptyAI/StdInc.h000066400000000000000000000003541342332007200203120ustar00rootroot00000000000000#pragma once #include "../../Global.h" // This header should be treated as a pre compiled header file(PCH) in the compiler building settings. // Here you can add specific libraries and macros which are specific to this project.vcmi-0.99+git20190113.f06c8a87/AI/EmptyAI/exp_funcs.cpp000066400000000000000000000011131342332007200214450ustar00rootroot00000000000000/* * exp_funcs.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CEmptyAI.h" std::set ais; extern "C" DLL_EXPORT int GetGlobalAiVersion() { return AI_INTERFACE_VER; } extern "C" DLL_EXPORT void GetAiName(char* name) { strcpy(name,NAME); } extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr &out) { out = std::make_shared(); } vcmi-0.99+git20190113.f06c8a87/AI/FuzzyLite.cbp000066400000000000000000000376311342332007200201100ustar00rootroot00000000000000 vcmi-0.99+git20190113.f06c8a87/AI/GeniusAI.brain000066400000000000000000000123421342332007200201260ustar00rootroot00000000000000o 34 16 17 R o 34 16 17 R o 47 16 R o 101 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 24 R o 98 16 17 R o 98 16 17 R o 100 16 R o 38 16 R o 61 16 R o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 R o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 R o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 R o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 R o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 R o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 R o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 R o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 R o 28 16 R o 81 16 R o 83 25 R o 31 16 R o 57 24 R o 23 16 R o 102 16 R o 37 24 R o 51 16 R t 0 0 25 R t 0 1 25 R t 0 2 25 R t 0 3 25 R t 0 4 25 R t 0 5 25 R t 0 6 25 R t 0 7 25 R t 0 8 25 R t 0 9 25 R t 0 10 25 R t 0 11 25 R t 0 12 25 R t 0 13 25 R t 0 14 25 R t 0 15 25 R t 0 16 25 R t 0 17 25 R t 0 18 25 R t 0 19 25 R t 0 20 25 R t 0 21 25 R t 0 22 25 R t 0 23 25 R t 0 30 25 R t 0 31 25 R t 0 32 25 R t 0 33 25 R t 0 34 25 R t 0 35 25 R t 0 36 25 R t 0 37 25 R t 0 38 25 R t 0 39 25 R t 0 40 25 R t 0 41 25 R t 0 42 25 R t 0 43 25 R t 1 0 25 R t 1 1 25 R t 1 2 25 R t 1 3 25 R t 1 4 25 R t 1 5 25 R t 1 6 25 R t 1 7 25 R t 1 8 25 R t 1 9 25 R t 1 10 25 R t 1 11 25 R t 1 12 25 R t 1 13 25 R t 1 14 25 R t 1 15 25 R t 1 16 25 R t 1 17 25 R t 1 18 25 R t 1 19 25 R t 1 20 25 R t 1 21 25 R t 1 22 25 R t 1 23 25 R t 1 30 25 R t 1 31 25 R t 1 32 25 R t 1 33 25 R t 1 34 25 R t 1 35 25 R t 1 36 25 R t 1 37 25 R t 1 38 25 R t 1 39 25 R t 1 40 25 R t 1 41 25 R t 1 42 25 R t 1 43 25 R t 2 0 25 R t 2 1 25 R t 2 2 25 R t 2 3 25 R t 2 4 25 R t 2 5 25 R t 2 6 25 R t 2 7 25 R t 2 8 25 R t 2 9 25 R t 2 10 25 R t 2 11 25 R t 2 12 25 R t 2 13 25 R t 2 14 25 R t 2 15 25 R t 2 16 25 R t 2 17 25 R t 2 18 25 R t 2 19 25 R t 2 20 25 R t 2 21 25 R t 2 22 25 R t 2 23 25 R t 2 30 25 R t 2 31 25 R t 2 32 25 R t 2 33 25 R t 2 34 25 R t 2 35 25 R t 2 36 25 R t 2 37 25 R t 2 38 25 R t 2 39 25 R t 2 40 25 R t 2 41 25 R t 2 42 25 R t 2 43 25 R t 3 0 25 R t 3 1 25 R t 3 2 25 R t 3 3 25 R t 3 4 25 R t 3 5 25 R t 3 6 25 R t 3 7 25 R t 3 8 25 R t 3 9 25 R t 3 10 25 R t 3 11 25 R t 3 12 25 R t 3 13 25 R t 3 14 25 R t 3 15 25 R t 3 16 25 R t 3 17 25 R t 3 18 25 R t 3 19 25 R t 3 20 25 R t 3 21 25 R t 3 22 25 R t 3 23 25 R t 3 30 25 R t 3 31 25 R t 3 32 25 R t 3 33 25 R t 3 34 25 R t 3 35 25 R t 3 36 25 R t 3 37 25 R t 3 38 25 R t 3 39 25 R t 3 40 25 R t 3 41 25 R t 3 42 25 R t 3 43 25 R t 4 0 25 R t 4 1 25 R t 4 2 25 R t 4 3 25 R t 4 4 25 R t 4 5 25 R t 4 6 25 R t 4 7 25 R t 4 8 25 R t 4 9 25 R t 4 10 25 R t 4 11 25 R t 4 12 25 R t 4 13 25 R t 4 14 25 R t 4 15 25 R t 4 16 25 R t 4 17 25 R t 4 18 25 R t 4 19 25 R t 4 20 25 R t 4 21 25 R t 4 22 25 R t 4 23 25 R t 4 30 25 R t 4 31 25 R t 4 32 25 R t 4 33 25 R t 4 34 25 R t 4 35 25 R t 4 36 25 R t 4 37 25 R t 4 38 25 R t 4 39 25 R t 4 40 25 R t 4 41 25 R t 4 42 25 R t 4 43 25 R t 5 0 25 R t 5 1 25 R t 5 2 25 R t 5 3 25 R t 5 4 25 R t 5 5 25 R t 5 6 25 R t 5 7 25 R t 5 8 25 R t 5 9 25 R t 5 10 25 R t 5 11 25 R t 5 12 25 R t 5 13 25 R t 5 14 25 R t 5 15 25 R t 5 16 25 R t 5 17 25 R t 5 18 25 R t 5 19 25 R t 5 20 25 R t 5 21 25 R t 5 22 25 R t 5 23 25 R t 5 30 25 R t 5 31 25 R t 5 32 25 R t 5 33 25 R t 5 34 25 R t 5 35 25 R t 5 36 25 R t 5 37 25 R t 5 38 25 R t 5 39 25 R t 5 40 25 R t 5 41 25 R t 5 42 25 R t 5 43 25 R t 6 0 25 R t 6 1 25 R t 6 2 25 R t 6 3 25 R t 6 4 25 R t 6 5 25 R t 6 6 25 R t 6 7 25 R t 6 8 25 R t 6 9 25 R t 6 10 25 R t 6 11 25 R t 6 12 25 R t 6 13 25 R t 6 14 25 R t 6 15 25 R t 6 16 25 R t 6 17 25 R t 6 18 25 R t 6 19 25 R t 6 20 25 R t 6 21 25 R t 6 22 25 R t 6 23 25 R t 6 30 25 R t 6 31 25 R t 6 32 25 R t 6 33 25 R t 6 34 25 R t 6 35 25 R t 6 36 25 R t 6 37 25 R t 6 38 25 R t 6 39 25 R t 6 40 25 R t 6 41 25 R t 6 42 25 R t 6 43 25 R t 7 0 25 R t 7 1 25 R t 7 2 25 R t 7 3 25 R t 7 4 25 R t 7 5 25 R t 7 6 25 R t 7 7 25 R t 7 8 25 R t 7 9 25 R t 7 10 25 R t 7 11 25 R t 7 12 25 R t 7 13 25 R t 7 14 25 R t 7 15 25 R t 7 16 25 R t 7 17 25 R t 7 18 25 R t 7 19 25 R t 7 20 25 R t 7 21 25 R t 7 22 25 R t 7 23 25 R t 7 30 25 R t 7 31 25 R t 7 32 25 R t 7 33 25 R t 7 34 25 R t 7 35 25 R t 7 36 25 R t 7 37 25 R t 7 38 25 R t 7 39 25 R t 7 40 25 R t 7 41 25 R t 7 42 25 R t 7 43 25 R t 8 0 25 R t 8 1 25 R t 8 2 25 R t 8 3 25 R t 8 4 25 R t 8 5 25 R t 8 6 25 R t 8 7 25 R t 8 8 25 R t 8 9 25 R t 8 10 25 R t 8 11 25 R t 8 12 25 R t 8 13 25 R t 8 14 25 R t 8 15 25 R t 8 16 25 R t 8 17 25 R t 8 18 25 R t 8 19 25 R t 8 20 25 R t 8 21 25 R t 8 22 25 R t 8 23 25 R t 8 30 25 R t 8 31 25 R t 8 32 25 R t 8 33 25 R t 8 34 25 R t 8 35 25 R t 8 36 25 R t 8 37 25 R t 8 38 25 R t 8 39 25 R t 8 40 25 R t 8 41 25 R t 8 42 25 R t 8 43 25 R vcmi-0.99+git20190113.f06c8a87/AI/StupidAI/000077500000000000000000000000001342332007200171255ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/AI/StupidAI/CMakeLists.txt000066400000000000000000000011761342332007200216720ustar00rootroot00000000000000include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_HOME_DIRECTORY}/include ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib) set(stupidAI_SRCS StdInc.cpp main.cpp StupidAI.cpp ) set(stupidAI_HEADERS StdInc.h StupidAI.h ) assign_source_group(${stupidAI_SRCS} ${stupidAI_HEADERS}) add_library(StupidAI SHARED ${stupidAI_SRCS} ${stupidAI_HEADERS}) target_link_libraries(StupidAI vcmi) vcmi_set_output_dir(StupidAI "AI") set_target_properties(StupidAI PROPERTIES ${PCH_PROPERTIES}) cotire(StupidAI) install(TARGETS StupidAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR}) vcmi-0.99+git20190113.f06c8a87/AI/StupidAI/StdInc.cpp000066400000000000000000000000671342332007200210200ustar00rootroot00000000000000// Creates the precompiled header #include "StdInc.h" vcmi-0.99+git20190113.f06c8a87/AI/StupidAI/StdInc.h000066400000000000000000000003541342332007200204640ustar00rootroot00000000000000#pragma once #include "../../Global.h" // This header should be treated as a pre compiled header file(PCH) in the compiler building settings. // Here you can add specific libraries and macros which are specific to this project.vcmi-0.99+git20190113.f06c8a87/AI/StupidAI/StupidAI.cbp000066400000000000000000000043621342332007200213020ustar00rootroot00000000000000 vcmi-0.99+git20190113.f06c8a87/AI/StupidAI/StupidAI.cpp000066400000000000000000000220141342332007200213120ustar00rootroot00000000000000/* * StupidAI.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "../../lib/AI_Base.h" #include "StupidAI.h" #include "../../lib/CStack.h" #include "../../CCallback.h" #include "../../lib/CCreatureHandler.h" static std::shared_ptr cbc; CStupidAI::CStupidAI() : side(-1) { print("created"); } CStupidAI::~CStupidAI() { print("destroyed"); } void CStupidAI::init(std::shared_ptr CB) { print("init called, saving ptr to IBattleCallback"); cbc = cb = CB; } void CStupidAI::actionFinished(const BattleAction &action) { print("actionFinished called"); } void CStupidAI::actionStarted(const BattleAction &action) { print("actionStarted called"); } struct EnemyInfo { const CStack * s; int adi, adr; std::vector attackFrom; //for melee fight EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0) {} void calcDmg(const CStack * ourStack) { TDmgRange retal, dmg = cbc->battleEstimateDamage(ourStack, s, &retal); adi = (dmg.first + dmg.second) / 2; adr = (retal.first + retal.second) / 2; } bool operator==(const EnemyInfo& ei) const { return s == ei.s; } }; bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2) { return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr); } namespace { int distToNearestNeighbour(BattleHex hex, const ReachabilityInfo::TDistances& dists, BattleHex *chosenHex = nullptr) { int ret = 1000000; for(auto & n: hex.neighbouringTiles()) { if(dists[n] >= 0 && dists[n] < ret) { ret = dists[n]; if(chosenHex) *chosenHex = n; } } return ret; } bool isCloser(const EnemyInfo & ei1, const EnemyInfo & ei2, const ReachabilityInfo::TDistances & dists) { return distToNearestNeighbour(ei1.s->getPosition(), dists) < distToNearestNeighbour(ei2.s->getPosition(), dists); } } static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const BattleHex &h2) { int shooters[2] = {0}; //count of shooters on hexes for(int i = 0; i < 2; i++) for (auto & neighbour : (i ? h2 : h1).neighbouringTiles()) if(const CStack *s = cbc->battleGetStackByPos(neighbour)) if(s->getCreature()->isShooting()) shooters[i]++; return shooters[0] < shooters[1]; } BattleAction CStupidAI::activeStack( const CStack * stack ) { //boost::this_thread::sleep(boost::posix_time::seconds(2)); print("activeStack called for " + stack->nodeName()); auto dists = cb->battleGetDistances(stack, stack->getPosition()); std::vector enemiesShootable, enemiesReachable, enemiesUnreachable; if(stack->type->idNumber == CreatureID::CATAPULT) { BattleAction attack; static const std::vector wallHexes = {50, 183, 182, 130, 78, 29, 12, 95}; auto seletectedHex = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault()); attack.aimToHex(seletectedHex); attack.actionType = EActionType::CATAPULT; attack.side = side; attack.stackNumber = stack->ID; return attack; } else if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON)) { return BattleAction::makeDefend(stack); } for (const CStack *s : cb->battleGetStacks(CBattleCallback::ONLY_ENEMY)) { if(cb->battleCanShoot(stack, s->getPosition())) { enemiesShootable.push_back(s); } else { std::vector avHexes = cb->battleGetAvailableHexes(stack); for (BattleHex hex : avHexes) { if(CStack::isMeleeAttackPossible(stack, s, hex)) { std::vector::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s); if(i == enemiesReachable.end()) { enemiesReachable.push_back(s); i = enemiesReachable.begin() + (enemiesReachable.size() - 1); } i->attackFrom.push_back(hex); } } if(!vstd::contains(enemiesReachable, s) && s->getPosition().isValid()) enemiesUnreachable.push_back(s); } } for ( auto & enemy : enemiesReachable ) enemy.calcDmg( stack ); for ( auto & enemy : enemiesShootable ) enemy.calcDmg( stack ); if(enemiesShootable.size()) { const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable); return BattleAction::makeShotAttack(stack, ei.s); } else if(enemiesReachable.size()) { const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable); return BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), &willSecondHexBlockMoreEnemyShooters)); } else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies { assert(enemiesUnreachable.size()); const EnemyInfo &ei= *std::min_element(enemiesUnreachable.begin(), enemiesUnreachable.end(), std::bind(isCloser, _1, _2, std::ref(dists))); assert(ei.s); if(distToNearestNeighbour(ei.s->getPosition(), dists) < GameConstants::BFIELD_SIZE) { return goTowards(stack, ei.s->getPosition()); } } return BattleAction::makeDefend(stack); } void CStupidAI::battleAttack(const BattleAttack *ba) { print("battleAttack called"); } void CStupidAI::battleStacksAttacked(const std::vector & bsa, const std::vector & battleLog) { print("battleStacksAttacked called"); } void CStupidAI::battleEnd(const BattleResult *br) { print("battleEnd called"); } // void CStupidAI::battleResultsApplied() // { // print("battleResultsApplied called"); // } void CStupidAI::battleNewRoundFirst(int round) { print("battleNewRoundFirst called"); } void CStupidAI::battleNewRound(int round) { print("battleNewRound called"); } void CStupidAI::battleStackMoved(const CStack * stack, std::vector dest, int distance) { print("battleStackMoved called"); } void CStupidAI::battleSpellCast(const BattleSpellCast *sc) { print("battleSpellCast called"); } void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse) { print("battleStacksEffectsSet called"); } void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side) { print("battleStart called"); side = Side; } void CStupidAI::battleCatapultAttacked(const CatapultAttack & ca) { print("battleCatapultAttacked called"); } void CStupidAI::print(const std::string &text) const { logAi->trace("CStupidAI [%p]: %s", this, text); } BattleAction CStupidAI::goTowards(const CStack * stack, BattleHex destination) { assert(destination.isValid()); auto reachability = cb->getReachability(stack); auto avHexes = cb->battleGetAvailableHexes(reachability, stack); if(vstd::contains(avHexes, destination)) return BattleAction::makeMove(stack, destination); auto destNeighbours = destination.neighbouringTiles(); if(vstd::contains_if(destNeighbours, [&](BattleHex n) { return stack->coversPos(destination); })) { logAi->warn("Warning: already standing on neighbouring tile!"); //We shouldn't even be here... return BattleAction::makeDefend(stack); } vstd::erase_if(destNeighbours, [&](BattleHex hex){ return !reachability.accessibility.accessible(hex, stack); }); if(!avHexes.size() || !destNeighbours.size()) //we are blocked or dest is blocked { print("goTowards: Stack cannot move! That's " + stack->nodeName()); return BattleAction::makeDefend(stack); } if(stack->hasBonusOfType(Bonus::FLYING)) { // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. // We just check all available hexes and pick the one closest to the target. auto distToDestNeighbour = [&](BattleHex hex) -> int { auto nearestNeighbourToHex = vstd::minElementByFun(destNeighbours, [&](BattleHex a) { return BattleHex::getDistance(a, hex); }); return BattleHex::getDistance(*nearestNeighbourToHex, hex); }; auto nearestAvailableHex = vstd::minElementByFun(avHexes, distToDestNeighbour); return BattleAction::makeMove(stack, *nearestAvailableHex); } else { BattleHex bestNeighbor = destination; if(distToNearestNeighbour(destination, reachability.distances, &bestNeighbor) > GameConstants::BFIELD_SIZE) { print("goTowards: Cannot reach"); return BattleAction::makeDefend(stack); } BattleHex currentDest = bestNeighbor; while(1) { assert(currentDest.isValid()); if(vstd::contains(avHexes, currentDest)) return BattleAction::makeMove(stack, currentDest); currentDest = reachability.predecessors[currentDest]; } } } void CStupidAI::saveGame(BinarySerializer & h, const int version) { //TODO to be implemented with saving/loading during the battles assert(0); } void CStupidAI::loadGame(BinaryDeserializer & h, const int version) { //TODO to be implemented with saving/loading during the battles assert(0); } vcmi-0.99+git20190113.f06c8a87/AI/StupidAI/StupidAI.h000066400000000000000000000046651342332007200207730ustar00rootroot00000000000000/* * StupidAI.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/battle/BattleHex.h" class CStupidAI : public CBattleGameInterface { int side; std::shared_ptr cb; void print(const std::string &text) const; public: CStupidAI(); ~CStupidAI(); void init(std::shared_ptr CB) override; void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack void battleStacksAttacked(const std::vector & bsa, const std::vector & battleLog) override; //called when stack receives damage (after battleAttack()) void battleEnd(const BattleResult *br) override; //void battleResultsApplied() override; //called when all effects of last battle are applied void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn void battleStackMoved(const CStack * stack, std::vector dest, int distance) override; void battleSpellCast(const BattleSpellCast *sc) override; void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks //void battleTriggerEffect(const BattleTriggerEffect & bte) override; void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack BattleAction goTowards(const CStack * stack, BattleHex hex ); virtual void saveGame(BinarySerializer & h, const int version) override; virtual void loadGame(BinaryDeserializer & h, const int version) override; }; vcmi-0.99+git20190113.f06c8a87/AI/StupidAI/StupidAI.vcxproj000066400000000000000000000175201342332007200222310ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B} StupidAI 10.0.17763.0 DynamicLibrary true MultiByte v140_xp DynamicLibrary true MultiByte v140_xp DynamicLibrary false true MultiByte v141 DynamicLibrary false true MultiByte v140_xp $(VCMI_Out)\AI\ $(VCMI_Out)\AI\ $(VCMI_Out)/AI $(VCMI_Out)\AI\ Use StdInc.h /Zm150 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) -Zm150 %(AdditionalOptions) ..\..\..\libs;..\..;.. Use StdInc.h /Zm150 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) Use StdInc.h true VCMI_lib.lib;%(AdditionalDependencies) $(VCMI_Out) Use StdInc.h /Zm150 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) %(PreprocessorDefinitions) Create StdInc.h Create Create Create vcmi-0.99+git20190113.f06c8a87/AI/StupidAI/main.cpp000066400000000000000000000013741342332007200205620ustar00rootroot00000000000000/* * main.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "../../lib/AI_Base.h" #include "StupidAI.h" #ifdef __GNUC__ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif static const char *g_cszAiName = "Stupid AI 0.1"; extern "C" DLL_EXPORT int GetGlobalAiVersion() { return AI_INTERFACE_VER; } extern "C" DLL_EXPORT void GetAiName(char* name) { strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); } extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) { out = std::make_shared(); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/000077500000000000000000000000001342332007200161655ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/AI/VCAI/AIUtility.cpp000066400000000000000000000315751342332007200205610ustar00rootroot00000000000000/* * AIUtility.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "AIUtility.h" #include "VCAI.h" #include "FuzzyHelper.h" #include "Goals/Goals.h" #include "../../lib/UnlockGuard.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/CBank.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CQuest.h" #include "../../lib/mapping/CMapDefines.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; //extern static const int3 dirs[8]; const CGObjectInstance * ObjectIdRef::operator->() const { return cb->getObj(id, false); } ObjectIdRef::operator const CGObjectInstance *() const { return cb->getObj(id, false); } ObjectIdRef::operator bool() const { return cb->getObj(id, false); } ObjectIdRef::ObjectIdRef(ObjectInstanceID _id) : id(_id) { } ObjectIdRef::ObjectIdRef(const CGObjectInstance * obj) : id(obj->id) { } bool ObjectIdRef::operator<(const ObjectIdRef & rhs) const { return id < rhs.id; } HeroPtr::HeroPtr(const CGHeroInstance * H) { if(!H) { //init from nullptr should equal to default init *this = HeroPtr(); return; } h = H; name = h->name; hid = H->id; // infosCount[ai->playerID][hid]++; } HeroPtr::HeroPtr() { h = nullptr; hid = ObjectInstanceID(); } HeroPtr::~HeroPtr() { // if(hid >= 0) // infosCount[ai->playerID][hid]--; } bool HeroPtr::operator<(const HeroPtr & rhs) const { return hid < rhs.hid; } const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const { //TODO? check if these all assertions every time we get info about hero affect efficiency // //behave terribly when attempting unauthorized access to hero that is not ours (or was lost) assert(doWeExpectNull || h); if(h) { auto obj = cb->getObj(hid); const bool owned = obj && obj->tempOwner == ai->playerID; if(doWeExpectNull && !owned) { return nullptr; } else { assert(obj); assert(owned); } } return h; } const CGHeroInstance * HeroPtr::operator->() const { return get(); } bool HeroPtr::validAndSet() const { return get(true); } const CGHeroInstance * HeroPtr::operator*() const { return get(); } bool HeroPtr::operator==(const HeroPtr & rhs) const { return h == rhs.get(true); } void foreach_tile_pos(std::function foo) { // some micro-optimizations since this function gets called a LOT // callback pointer is thread-specific and slow to retrieve -> read map size only once int3 mapSize = cb->getMapSize(); for(int i = 0; i < mapSize.x; i++) { for(int j = 0; j < mapSize.y; j++) { for(int k = 0; k < mapSize.z; k++) foo(int3(i, j, k)); } } } void foreach_tile_pos(CCallback * cbp, std::function foo) { int3 mapSize = cbp->getMapSize(); for(int i = 0; i < mapSize.x; i++) { for(int j = 0; j < mapSize.y; j++) { for(int k = 0; k < mapSize.z; k++) foo(cbp, int3(i, j, k)); } } } void foreach_neighbour(const int3 & pos, std::function foo) { CCallback * cbp = cb.get(); // avoid costly retrieval of thread-specific pointer for(const int3 & dir : int3::getDirs()) { const int3 n = pos + dir; if(cbp->isInTheMap(n)) foo(pos + dir); } } void foreach_neighbour(CCallback * cbp, const int3 & pos, std::function foo) { for(const int3 & dir : int3::getDirs()) { const int3 n = pos + dir; if(cbp->isInTheMap(n)) foo(cbp, pos + dir); } } bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const { const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos()); const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos()); return ln->cost < rn->cost; } ui64 evaluateDanger(crint3 tile) { const TerrainTile * t = cb->getTile(tile, false); if(!t) //we can know about guard but can't check its tile (the edge of fow) return 190000000; //MUCH ui64 objectDanger = 0; ui64 guardDanger = 0; auto visObjs = cb->getVisitableObjs(tile); if(visObjs.size()) objectDanger = evaluateDanger(visObjs.back()); int3 guardPos = cb->getGuardingCreaturePosition(tile); if(guardPos.x >= 0 && guardPos != tile) guardDanger = evaluateDanger(guardPos); //TODO mozna odwiedzic blockvis nie ruszajac straznika return std::max(objectDanger, guardDanger); } ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor) { const TerrainTile * t = cb->getTile(tile, false); if(!t) //we can know about guard but can't check its tile (the edge of fow) return 190000000; //MUCH ui64 objectDanger = 0; ui64 guardDanger = 0; auto visitableObjects = cb->getVisitableObjs(tile); // in some scenarios hero happens to be "under" the object (eg town). Then we consider ONLY the hero. if(vstd::contains_if(visitableObjects, objWithID)) { vstd::erase_if(visitableObjects, [](const CGObjectInstance * obj) { return !objWithID(obj); }); } if(const CGObjectInstance * dangerousObject = vstd::backOrNull(visitableObjects)) { objectDanger = evaluateDanger(dangerousObject); //unguarded objects can also be dangerous or unhandled if(objectDanger) { //TODO: don't downcast objects AI shouldn't know about! auto armedObj = dynamic_cast(dangerousObject); if(armedObj) { float tacticalAdvantage = fh->tacticalAdvantageEngine.getTacticalAdvantage(visitor, armedObj); objectDanger *= tacticalAdvantage; //this line tends to go infinite for allied towns (?) } } if(dangerousObject->ID == Obj::SUBTERRANEAN_GATE) { //check guard on the other side of the gate auto it = ai->knownSubterraneanGates.find(dangerousObject); if(it != ai->knownSubterraneanGates.end()) { auto guards = cb->getGuardingCreatures(it->second->visitablePos()); for(auto cre : guards) { vstd::amax(guardDanger, evaluateDanger(cre) * fh->tacticalAdvantageEngine.getTacticalAdvantage(visitor, dynamic_cast(cre))); } } } } auto guards = cb->getGuardingCreatures(tile); for(auto cre : guards) { vstd::amax(guardDanger, evaluateDanger(cre) * fh->tacticalAdvantageEngine.getTacticalAdvantage(visitor, dynamic_cast(cre))); //we are interested in strongest monster around } //TODO mozna odwiedzic blockvis nie ruszajac straznika return std::max(objectDanger, guardDanger); } ui64 evaluateDanger(const CGObjectInstance * obj) { if(obj->tempOwner < PlayerColor::PLAYER_LIMIT && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat return 0; switch(obj->ID) { case Obj::HERO: { InfoAboutHero iah; cb->getHeroInfo(obj, iah); return iah.army.getStrength(); } case Obj::TOWN: case Obj::GARRISON: case Obj::GARRISON2: { InfoAboutTown iat; cb->getTownInfo(obj, iat); return iat.army.getStrength(); } case Obj::MONSTER: { //TODO!!!!!!!! const CGCreature * cre = dynamic_cast(obj); return cre->getArmyStrength(); } case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR4: { const CGDwelling * d = dynamic_cast(obj); return d->getArmyStrength(); } case Obj::MINE: case Obj::ABANDONED_MINE: { const CArmedInstance * a = dynamic_cast(obj); return a->getArmyStrength(); } case Obj::CRYPT: //crypt case Obj::CREATURE_BANK: //crebank case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck case Obj::DERELICT_SHIP: //derelict ship // case Obj::PYRAMID: return fh->estimateBankDanger(dynamic_cast(obj)); case Obj::PYRAMID: { if(obj->subID == 0) return fh->estimateBankDanger(dynamic_cast(obj)); else return 0; } default: return 0; } } bool compareDanger(const CGObjectInstance * lhs, const CGObjectInstance * rhs) { return evaluateDanger(lhs) < evaluateDanger(rhs); } bool isSafeToVisit(HeroPtr h, crint3 tile) { return isSafeToVisit(h, evaluateDanger(tile)); } bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength) { const ui64 heroStrength = h->getTotalStrength(); if(dangerStrength) { if(heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength) { return true; } else { return false; } } return true; //there's no danger } bool isObjectRemovable(const CGObjectInstance * obj) { //FIXME: move logic to object property! switch (obj->ID) { case Obj::MONSTER: case Obj::RESOURCE: case Obj::CAMPFIRE: case Obj::TREASURE_CHEST: case Obj::ARTIFACT: case Obj::BORDERGUARD: case Obj::FLOTSAM: case Obj::PANDORAS_BOX: case Obj::OCEAN_BOTTLE: case Obj::SEA_CHEST: case Obj::SHIPWRECK_SURVIVOR: case Obj::SPELL_SCROLL: return true; break; default: return false; break; } } bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater) { // TODO: Such information should be provided by pathfinder // Tile must be free or with unoccupied boat if(!t->blocked) { return true; } else if(!fromWater) // do not try to board when in water sector { if(t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT) return true; } return false; } bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder { if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE) return false; auto gate = dynamic_cast(cb->getTile(tileToHit)->topVisitableObj()); return !gate->passableFor(ai->playerID); } bool isBlockVisitObj(const int3 & pos) { if(auto obj = cb->getTopObj(pos)) { if(obj->blockVisit) //we can't stand on that object return true; } return false; } creInfo infoFromDC(const dwellingContent & dc) { creInfo ci; ci.count = dc.first; ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed if (ci.creID != -1) { ci.cre = VLC->creh->creatures[ci.creID]; ci.level = ci.cre->level; //this is cretaure tier, while tryRealize expects dwelling level. Ignore. } else { ci.cre = nullptr; ci.level = 0; } return ci; } ui64 howManyReinforcementsCanBuy(const CArmedInstance * h, const CGDwelling * t) { ui64 aivalue = 0; TResources availableRes = cb->getResourceAmount(); int freeHeroSlots = GameConstants::ARMY_SIZE - h->stacksCount(); for(auto const dc : t->creatures) { creInfo ci = infoFromDC(dc); if(!ci.count || ci.creID == -1) continue; vstd::amin(ci.count, availableRes / ci.cre->cost); //max count we can afford if(ci.count && ci.creID != -1) //valid creature at this level { //can be merged with another stack? SlotID dst = h->getSlotFor(ci.creID); if(!h->hasStackAtSlot(dst)) //need another new slot for this stack { if(!freeHeroSlots) //no more place for stacks continue; else freeHeroSlots--; //new slot will be occupied } //we found matching occupied or free slot aivalue += ci.count * ci.cre->AIValue; availableRes -= ci.cre->cost * ci.count; } } return aivalue; } ui64 howManyReinforcementsCanGet(const CArmedInstance * h, const CGTownInstance * t) { ui64 ret = 0; int freeHeroSlots = GameConstants::ARMY_SIZE - h->stacksCount(); std::vector toMove; for(auto const slot : t->Slots()) { //can be merged woth another stack? SlotID dst = h->getSlotFor(slot.second->getCreatureID()); if(h->hasStackAtSlot(dst)) ret += t->getPower(slot.first); else toMove.push_back(slot.second); } boost::sort(toMove, [](const CStackInstance * lhs, const CStackInstance * rhs) { return lhs->getPower() < rhs->getPower(); }); for(auto & stack : boost::adaptors::reverse(toMove)) { if(freeHeroSlots) { ret += stack->getPower(); freeHeroSlots--; } else break; } return ret; } bool compareHeroStrength(HeroPtr h1, HeroPtr h2) { return h1->getTotalStrength() < h2->getTotalStrength(); } bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2) { return a1->getArmyStrength() < a2->getArmyStrength(); } bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2) { auto art1 = a1->artType; auto art2 = a2->artType; if(art1->price == art2->price) return art1->valOfBonuses(Bonus::PRIMARY_SKILL) > art2->valOfBonuses(Bonus::PRIMARY_SKILL); else if(art1->price > art2->price) return true; else return false; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/AIUtility.h000066400000000000000000000120071342332007200202130ustar00rootroot00000000000000/* * AIUtility.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/VCMI_Lib.h" #include "../../lib/CBuildingHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CTownHandler.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CStopWatch.h" #include "../../lib/mapObjects/CObjectHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/CPathfinder.h" class CCallback; struct creInfo; typedef const int3 & crint3; typedef const std::string & crstring; typedef std::pair> dwellingContent; const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1; const int ACTUAL_RESOURCE_COUNT = 7; const int ALLOWED_ROAMING_HEROES = 8; //implementation-dependent extern const double SAFE_ATTACK_CONSTANT; extern const int GOLD_RESERVE; //provisional class for AI to store a reference to an owned hero object //checks if it's valid on access, should be used in place of const CGHeroInstance* struct DLL_EXPORT HeroPtr { const CGHeroInstance * h; ObjectInstanceID hid; public: std::string name; HeroPtr(); HeroPtr(const CGHeroInstance * H); ~HeroPtr(); operator bool() const { return validAndSet(); } bool operator<(const HeroPtr & rhs) const; const CGHeroInstance * operator->() const; const CGHeroInstance * operator*() const; //not that consistent with -> but all interfaces use CGHeroInstance*, so it's convenient bool operator==(const HeroPtr & rhs) const; bool operator!=(const HeroPtr & rhs) const { return !(*this == rhs); } const CGHeroInstance * get(bool doWeExpectNull = false) const; bool validAndSet() const; template void serialize(Handler & h, const int version) { h & this->h; h & hid; h & name; } }; enum BattleState { NO_BATTLE, UPCOMING_BATTLE, ONGOING_BATTLE, ENDING_BATTLE }; // AI lives in a dangerous world. CGObjectInstances under pointer may got deleted/hidden. // This class stores object id, so we can detect when we lose access to the underlying object. struct ObjectIdRef { ObjectInstanceID id; const CGObjectInstance * operator->() const; operator const CGObjectInstance *() const; operator bool() const; ObjectIdRef(ObjectInstanceID _id); ObjectIdRef(const CGObjectInstance * obj); bool operator<(const ObjectIdRef & rhs) const; template void serialize(Handler & h, const int version) { h & id; } }; struct TimeCheck { CStopWatch time; std::string txt; TimeCheck(crstring TXT) : txt(TXT) { } ~TimeCheck() { logAi->trace("Time of %s was %d ms.", txt, time.getDiff()); } }; //TODO: replace with vstd:: struct AtScopeExit { std::function foo; AtScopeExit(const std::function & FOO) : foo(FOO) {} ~AtScopeExit() { foo(); } }; class ObjsVector : public std::vector { }; template bool objWithID(const CGObjectInstance * obj) { return obj->ID == id; } struct creInfo { int count; CreatureID creID; CCreature * cre; int level; }; creInfo infoFromDC(const dwellingContent & dc); void foreach_tile_pos(std::function foo); void foreach_tile_pos(CCallback * cbp, std::function foo); // avoid costly retrieval of thread-specific pointer void foreach_neighbour(const int3 & pos, std::function foo); void foreach_neighbour(CCallback * cbp, const int3 & pos, std::function foo); // avoid costly retrieval of thread-specific pointer bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater); bool isBlockedBorderGate(int3 tileToHit); bool isBlockVisitObj(const int3 & pos); bool isWeeklyRevisitable(const CGObjectInstance * obj); bool shouldVisit(HeroPtr h, const CGObjectInstance * obj); ui64 evaluateDanger(const CGObjectInstance * obj); ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor); bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property! bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength); bool isSafeToVisit(HeroPtr h, crint3 tile); bool compareHeroStrength(HeroPtr h1, HeroPtr h2); bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2); bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2); ui64 howManyReinforcementsCanBuy(const CArmedInstance * h, const CGDwelling * t); ui64 howManyReinforcementsCanGet(const CArmedInstance * h, const CGTownInstance * t); class CDistanceSorter { const CGHeroInstance * hero; public: CDistanceSorter(const CGHeroInstance * hero) : hero(hero) { } bool operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const; }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/AIhelper.cpp000066400000000000000000000066341342332007200203730ustar00rootroot00000000000000/* * AIhelper.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "AIhelper.h" #include "ResourceManager.h" #include "BuildingManager.h" AIhelper::AIhelper() { resourceManager.reset(new ResourceManager()); buildingManager.reset(new BuildingManager()); pathfindingManager.reset(new PathfindingManager()); } AIhelper::~AIhelper() { } bool AIhelper::notifyGoalCompleted(Goals::TSubgoal goal) { return resourceManager->notifyGoalCompleted(goal); } void AIhelper::init(CPlayerSpecificInfoCallback * CB) { resourceManager->init(CB); buildingManager->init(CB); pathfindingManager->init(CB); } void AIhelper::setAI(VCAI * AI) { resourceManager->setAI(AI); buildingManager->setAI(AI); pathfindingManager->setAI(AI); } bool AIhelper::getBuildingOptions(const CGTownInstance * t) { return buildingManager->getBuildingOptions(t); } BuildingID AIhelper::getMaxPossibleGoldBuilding(const CGTownInstance * t) { return buildingManager->getMaxPossibleGoldBuilding(t); } boost::optional AIhelper::immediateBuilding() const { return buildingManager->immediateBuilding(); } boost::optional AIhelper::expensiveBuilding() const { return buildingManager->expensiveBuilding(); } boost::optional AIhelper::canBuildAnyStructure(const CGTownInstance * t, const std::vector & buildList, unsigned int maxDays) const { return buildingManager->canBuildAnyStructure(t, buildList, maxDays); } Goals::TSubgoal AIhelper::whatToDo(TResources & res, Goals::TSubgoal goal) { return resourceManager->whatToDo(res, goal); } Goals::TSubgoal AIhelper::whatToDo() const { return resourceManager->whatToDo(); } bool AIhelper::containsObjective(Goals::TSubgoal goal) const { return resourceManager->containsObjective(goal); } bool AIhelper::hasTasksLeft() const { return resourceManager->hasTasksLeft(); } bool AIhelper::removeOutdatedObjectives(std::function predicate) { return resourceManager->removeOutdatedObjectives(predicate); } bool AIhelper::canAfford(const TResources & cost) const { return resourceManager->canAfford(cost); } TResources AIhelper::reservedResources() const { return resourceManager->reservedResources(); } TResources AIhelper::freeResources() const { return resourceManager->freeResources(); } TResource AIhelper::freeGold() const { return resourceManager->freeGold(); } TResources AIhelper::allResources() const { return resourceManager->allResources(); } TResource AIhelper::allGold() const { return resourceManager->allGold(); } Goals::TGoalVec AIhelper::howToVisitTile(int3 tile) { return pathfindingManager->howToVisitTile(tile); } Goals::TGoalVec AIhelper::howToVisitObj(ObjectIdRef obj) { return pathfindingManager->howToVisitObj(obj); } Goals::TGoalVec AIhelper::howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy) { return pathfindingManager->howToVisitTile(hero, tile, allowGatherArmy); } Goals::TGoalVec AIhelper::howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy) { return pathfindingManager->howToVisitObj(hero, obj, allowGatherArmy); } std::vector AIhelper::getPathsToTile(HeroPtr hero, int3 tile) { return pathfindingManager->getPathsToTile(hero, tile); } void AIhelper::resetPaths() { pathfindingManager->resetPaths(); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/AIhelper.h000066400000000000000000000051251342332007200200320ustar00rootroot00000000000000/* * AIhelper.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once /* !!! Note: Include THIS file at the end of include list to avoid "undefined base class" error */ #include "ResourceManager.h" #include "BuildingManager.h" #include "Pathfinding/PathfindingManager.h" class ResourceManager; class BuildingManager; //indirection interface for various modules class DLL_EXPORT AIhelper : public IResourceManager, public IBuildingManager, public IPathfindingManager { friend class VCAI; friend struct SetGlobalState; //mess? std::shared_ptr resourceManager; std::shared_ptr buildingManager; std::shared_ptr pathfindingManager; //TODO: vector public: AIhelper(); ~AIhelper(); bool canAfford(const TResources & cost) const; TResources reservedResources() const override; TResources freeResources() const override; TResource freeGold() const override; TResources allResources() const override; TResource allGold() const override; Goals::TSubgoal whatToDo(TResources &res, Goals::TSubgoal goal) override; Goals::TSubgoal whatToDo() const override; bool containsObjective(Goals::TSubgoal goal) const override; bool hasTasksLeft() const override; bool removeOutdatedObjectives(std::function predicate) override; bool getBuildingOptions(const CGTownInstance * t) override; BuildingID getMaxPossibleGoldBuilding(const CGTownInstance * t); boost::optional immediateBuilding() const override; boost::optional expensiveBuilding() const override; boost::optional canBuildAnyStructure(const CGTownInstance * t, const std::vector & buildList, unsigned int maxDays = 7) const override; Goals::TGoalVec howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy = true) override; Goals::TGoalVec howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy = true) override; Goals::TGoalVec howToVisitTile(int3 tile) override; Goals::TGoalVec howToVisitObj(ObjectIdRef obj) override; std::vector getPathsToTile(HeroPtr hero, int3 tile) override; void resetPaths() override; STRONG_INLINE bool isTileAccessible(const HeroPtr & hero, const int3 & tile) { return pathfindingManager->isTileAccessible(hero, tile); } private: bool notifyGoalCompleted(Goals::TSubgoal goal) override; void init(CPlayerSpecificInfoCallback * CB) override; void setAI(VCAI * AI) override; }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/BuildingManager.cpp000066400000000000000000000217641342332007200217330ustar00rootroot00000000000000/* * BuildingManager.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "BuildingManager.h" #include "../../CCallback.h" #include "../../lib/mapObjects/MapObjects.h" bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays) { if (maxDays == 0) { logAi->warn("Request to build building %d in 0 days!", building.toEnum()); return false; } if (!vstd::contains(t->town->buildings, building)) return false; // no such building in town if (t->hasBuilt(building)) //Already built? Shouldn't happen in general return true; const CBuilding * buildPtr = t->town->buildings.at(building); auto toBuild = buildPtr->requirements.getFulfillmentCandidates([&](const BuildingID & buildID) { return t->hasBuilt(buildID); }); toBuild.push_back(building); for (BuildingID buildID : toBuild) { EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID); if (canBuild == EBuildingState::HAVE_CAPITAL || canBuild == EBuildingState::FORBIDDEN || canBuild == EBuildingState::NO_WATER) return false; //we won't be able to build this } if (maxDays && toBuild.size() > maxDays) return false; //TODO: calculate if we have enough resources to build it in maxDays? for (const auto & buildID : toBuild) { const CBuilding * b = t->town->buildings.at(buildID); EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID); if (canBuild == EBuildingState::ALLOWED) { PotentialBuilding pb; pb.bid = buildID; pb.price = t->getBuildingCost(buildID); immediateBuildings.push_back(pb); //these are checked again in try return true; } else if (canBuild == EBuildingState::PREREQUIRES) { // can happen when dependencies have their own missing dependencies if (tryBuildThisStructure(t, buildID, maxDays - 1)) return true; } else if (canBuild == EBuildingState::MISSING_BASE) { if (tryBuildThisStructure(t, b->upgrade, maxDays - 1)) return true; } else if (canBuild == EBuildingState::NO_RESOURCES) { //we may need to gather resources for those PotentialBuilding pb; pb.bid = buildID; pb.price = t->getBuildingCost(buildID); expensiveBuildings.push_back(pb); //these are checked again in try return false; } else return false; } return false; } bool BuildingManager::tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) { for (const auto & building : buildList) { if (t->hasBuilt(building)) continue; return tryBuildThisStructure(t, building, maxDays); } return false; //Can't build anything } boost::optional BuildingManager::canBuildAnyStructure(const CGTownInstance * t, const std::vector & buildList, unsigned int maxDays) const { for (const auto & building : buildList) { if (t->hasBuilt(building)) continue; switch (cb->canBuildStructure(t, building)) { case EBuildingState::ALLOWED: case EBuildingState::NO_RESOURCES: //TODO: allow this via optional parameter? return boost::optional(building); break; } } return boost::optional(); //Can't build anything } bool BuildingManager::tryBuildNextStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) { for (const auto & building : buildList) { if (t->hasBuilt(building)) continue; return tryBuildThisStructure(t, building, maxDays); } return false; //Nothing to build } void BuildingManager::init(CPlayerSpecificInfoCallback * CB) { cb = CB; } void BuildingManager::setAI(VCAI * AI) { ai = AI; } //Set of buildings for different goals. Does not include any prerequisites. static const std::vector essential = { BuildingID::TAVERN, BuildingID::TOWN_HALL }; static const std::vector basicGoldSource = { BuildingID::TOWN_HALL, BuildingID::CITY_HALL }; static const std::vector capitolAndRequirements = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::CAPITOL }; static const std::vector unitsSource = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3, BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 }; static const std::vector unitsUpgrade = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP, BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP }; static const std::vector unitGrowth = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR }; static const std::vector _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 }; static const std::vector extra = { BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, BuildingID::SPECIAL_4, BuildingID::SHIPYARD }; // all remaining buildings bool BuildingManager::getBuildingOptions(const CGTownInstance * t) { //TODO make *real* town development system //TODO: faction-specific development: use special buildings, build dwellings in better order, etc //TODO: build resource silo, defences when needed //Possible - allow "locking" on specific building (build prerequisites and then building itself) //TODO: There is some disabled building code in GatherTroops and GatherArmy - take it into account when enhancing building. For now AI works best with building only via Build goal. immediateBuildings.clear(); expensiveBuildings.clear(); //below algorithm focuses on economy growth at start of the game, saving money instead of build rushing is handled by Build goal //changing code blocks order will alter behavior by changing order of adding elements to immediateBuildings / expensiveBuildings TResources currentRes = cb->getResourceAmount(); TResources currentIncome = t->dailyIncome(); if(tryBuildAnyStructure(t, essential)) return true; //the more gold the better and less problems later //TODO: what about building mage guild / marketplace etc. with city hall disabled in editor? if(tryBuildNextStructure(t, basicGoldSource)) return true; //workaround for mantis #2696 - build capitol with separate algorithm if it is available if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL) { if(tryBuildNextStructure(t, capitolAndRequirements)) return true; } if(!t->hasBuilt(BuildingID::FORT)) //in vast majority of situations fort is top priority building if we already have city hall, TODO: unite with unitGrowth building chain if(tryBuildThisStructure(t, BuildingID::FORT)) return true; if (cb->getDate(Date::DAY_OF_WEEK) > 6) // last 2 days of week - try to focus on growth { if (tryBuildNextStructure(t, unitGrowth, 2)) return true; } //try building dwellings if (t->hasBuilt(BuildingID::FORT)) { if (tryBuildAnyStructure(t, unitsSource, 8 - cb->getDate(Date::DAY_OF_WEEK))) return true; } //try to upgrade dwelling for (int i = 0; i < unitsUpgrade.size(); i++) { if (t->hasBuilt(unitsSource[i]) && !t->hasBuilt(unitsUpgrade[i]) && t->hasBuilt(BuildingID::FORT)) { if (tryBuildThisStructure(t, unitsUpgrade[i])) return true; } } //remaining tasks if (tryBuildNextStructure(t, _spells)) return true; if (tryBuildAnyStructure(t, extra)) return true; //at the end, try to get and build any extra buildings with nonstandard slots (for example HotA 3rd level dwelling) std::vector extraBuildings; for (auto buildingInfo : t->town->buildings) { if (buildingInfo.first > 43) extraBuildings.push_back(buildingInfo.first); } if (tryBuildAnyStructure(t, extraBuildings)) return true; return false; } BuildingID BuildingManager::getMaxPossibleGoldBuilding(const CGTownInstance * t) { if(cb->canBuildStructure(t, BuildingID::CAPITOL) != EBuildingState::HAVE_CAPITAL && cb->canBuildStructure(t, BuildingID::CAPITOL) != EBuildingState::FORBIDDEN) return BuildingID::CAPITOL; else if(cb->canBuildStructure(t, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN) return BuildingID::CITY_HALL; else if(cb->canBuildStructure(t, BuildingID::TOWN_HALL) != EBuildingState::FORBIDDEN) return BuildingID::TOWN_HALL; else return BuildingID::VILLAGE_HALL; } boost::optional BuildingManager::immediateBuilding() const { if (immediateBuildings.size()) return boost::optional(immediateBuildings.front()); //back? whatever else return boost::optional(); } boost::optional BuildingManager::expensiveBuilding() const { if (expensiveBuildings.size()) return boost::optional(expensiveBuildings.front()); else return boost::optional(); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/BuildingManager.h000066400000000000000000000051031342332007200213650ustar00rootroot00000000000000/* * BuildingManager.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "AIUtility.h" #include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" #include "../../lib/CTownHandler.h" #include "../../lib/CBuildingHandler.h" #include "VCAI.h" struct DLL_EXPORT PotentialBuilding { BuildingID bid; TResources price; //days to build? }; class DLL_EXPORT IBuildingManager //: public: IAbstractManager { //info about town development public: virtual ~IBuildingManager() = default; virtual void init(CPlayerSpecificInfoCallback * CB) = 0; virtual void setAI(VCAI * AI) = 0; virtual bool getBuildingOptions(const CGTownInstance * t) = 0; virtual boost::optional immediateBuilding() const = 0; virtual boost::optional expensiveBuilding() const = 0; virtual boost::optional canBuildAnyStructure(const CGTownInstance * t, const std::vector & buildList, unsigned int maxDays) const = 0; }; class DLL_EXPORT BuildingManager : public IBuildingManager { friend class VCAI; friend class AIhelper; friend struct SetGlobalState; CPlayerSpecificInfoCallback * cb; //this is enough, but we downcast from CCallback VCAI * ai; public: //try build anything in given town, and execute resulting Goal if any bool getBuildingOptions(const CGTownInstance * t) override; BuildingID getMaxPossibleGoldBuilding(const CGTownInstance * t); boost::optional immediateBuilding() const override; boost::optional expensiveBuilding() const override; boost::optional canBuildAnyStructure(const CGTownInstance * t, const std::vector & buildList, unsigned int maxDays = 7) const override; protected: bool tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays = 7); //try build first unbuilt structure bool tryBuildThisStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays = 7); //try build ANY unbuilt structure bool tryBuildNextStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays = 7); private: //TODO: remember current town? std::vector immediateBuildings; //what we can build right now in current town std::vector expensiveBuildings; //what we coudl build but can't afford void init(CPlayerSpecificInfoCallback * CB) override; void setAI(VCAI * AI) override; }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/CMakeLists.txt000066400000000000000000000047371342332007200207400ustar00rootroot00000000000000if(FL_FOUND) include_directories(${FL_INCLUDE_DIRS}) else() include_directories(${CMAKE_HOME_DIRECTORY}/AI/FuzzyLite/fuzzylite) endif() include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_HOME_DIRECTORY}/include ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib) set(VCAI_SRCS StdInc.cpp Pathfinding/AIPathfinderConfig.cpp Pathfinding/AIPathfinder.cpp Pathfinding/AINodeStorage.cpp Pathfinding/PathfindingManager.cpp AIUtility.cpp AIhelper.cpp ResourceManager.cpp BuildingManager.cpp SectorMap.cpp BuildingManager.cpp MapObjectsEvaluator.cpp FuzzyEngines.cpp FuzzyHelper.cpp Goals/AbstractGoal.cpp Goals/BuildBoat.cpp Goals/Build.cpp Goals/BuildThis.cpp Goals/Explore.cpp Goals/GatherArmy.cpp Goals/GatherTroops.cpp Goals/BuyArmy.cpp Goals/AdventureSpellCast.cpp Goals/Win.cpp Goals/VisitTile.cpp Goals/VisitObj.cpp Goals/VisitHero.cpp Goals/CollectRes.cpp Goals/Trade.cpp Goals/RecruitHero.cpp Goals/Conquer.cpp Goals/ClearWayTo.cpp Goals/DigAtTile.cpp Goals/GetArtOfType.cpp Goals/FindObj.cpp Goals/CompleteQuest.cpp main.cpp VCAI.cpp ) set(VCAI_HEADERS StdInc.h Pathfinding/AIPathfinderConfig.h Pathfinding/AIPathfinder.h Pathfinding/AINodeStorage.h Pathfinding/PathfindingManager.h AIUtility.h AIhelper.h ResourceManager.h BuildingManager.h SectorMap.h BuildingManager.h MapObjectsEvaluator.h FuzzyEngines.h FuzzyHelper.h Goals/AbstractGoal.h Goals/CGoal.h Goals/Invalid.h Goals/BuildBoat.h Goals/Build.h Goals/BuildThis.h Goals/Explore.h Goals/GatherArmy.h Goals/GatherTroops.h Goals/BuyArmy.h Goals/AdventureSpellCast.h Goals/Win.h Goals/VisitTile.h Goals/VisitObj.h Goals/VisitHero.h Goals/CollectRes.h Goals/Trade.h Goals/RecruitHero.h Goals/Conquer.h Goals/ClearWayTo.h Goals/DigAtTile.h Goals/GetArtOfType.h Goals/FindObj.h Goals/CompleteQuest.h Goals/Goals.h VCAI.h ) assign_source_group(${VCAI_SRCS} ${VCAI_HEADERS}) if(ANDROID) # android compiles ai libs into main lib directly, so we skip this library and just reuse sources list return() endif() add_library(VCAI SHARED ${VCAI_SRCS} ${VCAI_HEADERS}) if(FL_FOUND) target_link_libraries(VCAI ${FL_LIBRARIES} vcmi) else() target_link_libraries(VCAI fl-static vcmi) endif() vcmi_set_output_dir(VCAI "AI") set_target_properties(VCAI PROPERTIES ${PCH_PROPERTIES}) cotire(VCAI) install(TARGETS VCAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR}) vcmi-0.99+git20190113.f06c8a87/AI/VCAI/FuzzyEngines.cpp000066400000000000000000000366341342332007200213450ustar00rootroot00000000000000/* * FuzzyEngines.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "FuzzyEngines.h" #include "Goals/Goals.h" #include "../../lib/mapObjects/MapObjects.h" #include "VCAI.h" #include "MapObjectsEvaluator.h" #define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us extern boost::thread_specific_ptr ai; engineBase::engineBase() { engine.addRuleBlock(&rules); } void engineBase::configure() { engine.configure("Minimum", "Maximum", "Minimum", "AlgebraicSum", "Centroid", "Proportional"); logAi->info(engine.toString()); } void engineBase::addRule(const std::string & txt) { rules.addRule(fl::Rule::parse(txt, &engine)); } struct armyStructure { float walkers, shooters, flyers; ui32 maxSpeed; }; armyStructure evaluateArmyStructure(const CArmedInstance * army) { ui64 totalStrenght = army->getArmyStrength(); double walkersStrenght = 0; double flyersStrenght = 0; double shootersStrenght = 0; ui32 maxSpeed = 0; static const CSelector selectorSHOOTER = Selector::type(Bonus::SHOOTER); static const std::string keySHOOTER = "type_"+std::to_string((int32_t)Bonus::SHOOTER); static const CSelector selectorFLYING = Selector::type(Bonus::FLYING); static const std::string keyFLYING = "type_"+std::to_string((int32_t)Bonus::FLYING); static const CSelector selectorSTACKS_SPEED = Selector::type(Bonus::STACKS_SPEED); static const std::string keySTACKS_SPEED = "type_"+std::to_string((int32_t)Bonus::STACKS_SPEED); for(auto s : army->Slots()) { bool walker = true; const CCreature * creature = s.second->type; if(creature->hasBonus(selectorSHOOTER, keySHOOTER)) { shootersStrenght += s.second->getPower(); walker = false; } if(creature->hasBonus(selectorFLYING, keyFLYING)) { flyersStrenght += s.second->getPower(); walker = false; } if(walker) walkersStrenght += s.second->getPower(); vstd::amax(maxSpeed, creature->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED)); } armyStructure as; as.walkers = walkersStrenght / totalStrenght; as.shooters = shootersStrenght / totalStrenght; as.flyers = flyersStrenght / totalStrenght; as.maxSpeed = maxSpeed; assert(as.walkers || as.flyers || as.shooters); return as; } float HeroMovementGoalEngineBase::calculateTurnDistanceInputValue(const Goals::AbstractGoal & goal) const { if(goal.evaluationContext.movementCost > 0) { return goal.evaluationContext.movementCost; } else { auto pathInfo = ai->myCb->getPathsInfo(goal.hero.h)->getPathInfo(goal.tile); return pathInfo->cost; } } TacticalAdvantageEngine::TacticalAdvantageEngine() { try { ourShooters = new fl::InputVariable("OurShooters"); ourWalkers = new fl::InputVariable("OurWalkers"); ourFlyers = new fl::InputVariable("OurFlyers"); enemyShooters = new fl::InputVariable("EnemyShooters"); enemyWalkers = new fl::InputVariable("EnemyWalkers"); enemyFlyers = new fl::InputVariable("EnemyFlyers"); //Tactical advantage calculation std::vector helper = { ourShooters, ourWalkers, ourFlyers, enemyShooters, enemyWalkers, enemyFlyers }; for(auto val : helper) { engine.addInputVariable(val); val->addTerm(new fl::Ramp("FEW", 0.6, 0.0)); val->addTerm(new fl::Ramp("MANY", 0.4, 1)); val->setRange(0.0, 1.0); } ourSpeed = new fl::InputVariable("OurSpeed"); enemySpeed = new fl::InputVariable("EnemySpeed"); helper = { ourSpeed, enemySpeed }; for(auto val : helper) { engine.addInputVariable(val); val->addTerm(new fl::Ramp("LOW", 6.5, 3)); val->addTerm(new fl::Triangle("MEDIUM", 5.5, 10.5)); val->addTerm(new fl::Ramp("HIGH", 8.5, 16)); val->setRange(0, 25); } castleWalls = new fl::InputVariable("CastleWalls"); engine.addInputVariable(castleWalls); { fl::Rectangle * none = new fl::Rectangle("NONE", CGTownInstance::NONE, CGTownInstance::NONE + (CGTownInstance::FORT - CGTownInstance::NONE) * 0.5f); castleWalls->addTerm(none); fl::Trapezoid * medium = new fl::Trapezoid("MEDIUM", (CGTownInstance::FORT - CGTownInstance::NONE) * 0.5f, CGTownInstance::FORT, CGTownInstance::CITADEL, CGTownInstance::CITADEL + (CGTownInstance::CASTLE - CGTownInstance::CITADEL) * 0.5f); castleWalls->addTerm(medium); fl::Ramp * high = new fl::Ramp("HIGH", CGTownInstance::CITADEL - 0.1, CGTownInstance::CASTLE); castleWalls->addTerm(high); castleWalls->setRange(CGTownInstance::NONE, CGTownInstance::CASTLE); } bankPresent = new fl::InputVariable("Bank"); engine.addInputVariable(bankPresent); { fl::Rectangle * termFalse = new fl::Rectangle("FALSE", 0.0, 0.5f); bankPresent->addTerm(termFalse); fl::Rectangle * termTrue = new fl::Rectangle("TRUE", 0.5f, 1); bankPresent->addTerm(termTrue); bankPresent->setRange(0, 1); } threat = new fl::OutputVariable("Threat"); engine.addOutputVariable(threat); threat->addTerm(new fl::Ramp("LOW", 1, MIN_AI_STRENGTH)); threat->addTerm(new fl::Triangle("MEDIUM", 0.8, 1.2)); threat->addTerm(new fl::Ramp("HIGH", 1, 1.5)); threat->setRange(MIN_AI_STRENGTH, 1.5); addRule("if OurShooters is MANY and EnemySpeed is LOW then Threat is LOW"); addRule("if OurShooters is MANY and EnemyShooters is FEW then Threat is LOW"); addRule("if OurSpeed is LOW and EnemyShooters is MANY then Threat is HIGH"); addRule("if OurSpeed is HIGH and EnemyShooters is MANY then Threat is LOW"); addRule("if OurWalkers is FEW and EnemyShooters is MANY then Threat is LOW"); addRule("if OurShooters is MANY and EnemySpeed is HIGH then Threat is HIGH"); //just to cover all cases addRule("if OurShooters is FEW and EnemySpeed is HIGH then Threat is MEDIUM"); addRule("if EnemySpeed is MEDIUM then Threat is MEDIUM"); addRule("if EnemySpeed is LOW and OurShooters is FEW then Threat is MEDIUM"); addRule("if Bank is TRUE and OurShooters is MANY then Threat is HIGH"); addRule("if Bank is TRUE and EnemyShooters is MANY then Threat is LOW"); addRule("if CastleWalls is HIGH and OurWalkers is MANY then Threat is HIGH"); addRule("if CastleWalls is HIGH and OurFlyers is MANY and OurShooters is MANY then Threat is MEDIUM"); addRule("if CastleWalls is MEDIUM and OurShooters is MANY and EnemyWalkers is MANY then Threat is LOW"); } catch(fl::Exception & pe) { logAi->error("initTacticalAdvantage: %s", pe.getWhat()); } configure(); } float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, const CArmedInstance * enemy) { float output = 1; try { armyStructure ourStructure = evaluateArmyStructure(we); armyStructure enemyStructure = evaluateArmyStructure(enemy); ourWalkers->setValue(ourStructure.walkers); ourShooters->setValue(ourStructure.shooters); ourFlyers->setValue(ourStructure.flyers); ourSpeed->setValue(ourStructure.maxSpeed); enemyWalkers->setValue(enemyStructure.walkers); enemyShooters->setValue(enemyStructure.shooters); enemyFlyers->setValue(enemyStructure.flyers); enemySpeed->setValue(enemyStructure.maxSpeed); bool bank = dynamic_cast(enemy); if(bank) bankPresent->setValue(1); else bankPresent->setValue(0); const CGTownInstance * fort = dynamic_cast(enemy); if(fort) castleWalls->setValue(fort->fortLevel()); else castleWalls->setValue(0); engine.process(); output = threat->getValue(); } catch(fl::Exception & fe) { logAi->error("getTacticalAdvantage: %s ", fe.getWhat()); } if(output < 0 || (output != output)) { fl::InputVariable * tab[] = { bankPresent, castleWalls, ourWalkers, ourShooters, ourFlyers, ourSpeed, enemyWalkers, enemyShooters, enemyFlyers, enemySpeed }; std::string names[] = { "bankPresent", "castleWalls", "ourWalkers", "ourShooters", "ourFlyers", "ourSpeed", "enemyWalkers", "enemyShooters", "enemyFlyers", "enemySpeed" }; std::stringstream log("Warning! Fuzzy engine doesn't cover this set of parameters: "); for(int i = 0; i < boost::size(tab); i++) log << names[i] << ": " << tab[i]->getValue() << " "; logAi->error(log.str()); assert(false); } return output; } //std::shared_ptr chooseSolution (std::vector> & vec) HeroMovementGoalEngineBase::HeroMovementGoalEngineBase() { try { strengthRatio = new fl::InputVariable("strengthRatio"); //hero must be strong enough to defeat guards heroStrength = new fl::InputVariable("heroStrength"); //we want to use weakest possible hero turnDistance = new fl::InputVariable("turnDistance"); //we want to use hero who is near missionImportance = new fl::InputVariable("lockedMissionImportance"); //we may want to preempt hero with low-priority mission value = new fl::OutputVariable("Value"); value->setMinimum(0); value->setMaximum(5); std::vector helper = { strengthRatio, heroStrength, turnDistance, missionImportance }; for(auto val : helper) { engine.addInputVariable(val); } engine.addOutputVariable(value); strengthRatio->addTerm(new fl::Ramp("LOW", SAFE_ATTACK_CONSTANT, 0)); strengthRatio->addTerm(new fl::Ramp("HIGH", SAFE_ATTACK_CONSTANT, SAFE_ATTACK_CONSTANT * 3)); strengthRatio->setRange(0, SAFE_ATTACK_CONSTANT * 3); //strength compared to our main hero heroStrength->addTerm(new fl::Ramp("LOW", 0.5, 0)); heroStrength->addTerm(new fl::Triangle("MEDIUM", 0.2, 0.8)); heroStrength->addTerm(new fl::Ramp("HIGH", 0.5, 1)); heroStrength->setRange(0.0, 1.0); turnDistance->addTerm(new fl::Ramp("SHORT", 0.5, 0)); turnDistance->addTerm(new fl::Triangle("MEDIUM", 0.1, 0.8)); turnDistance->addTerm(new fl::Ramp("LONG", 0.5, 10)); turnDistance->setRange(0.0, 10.0); missionImportance->addTerm(new fl::Ramp("LOW", 2.5, 0)); missionImportance->addTerm(new fl::Triangle("MEDIUM", 2, 3)); missionImportance->addTerm(new fl::Ramp("HIGH", 2.5, 5)); missionImportance->setRange(0.0, 5.0); //an issue: in 99% cases this outputs center of mass (2.5) regardless of actual input :/ //should be same as "mission Importance" to keep consistency value->addTerm(new fl::Ramp("LOW", 2.5, 0)); value->addTerm(new fl::Triangle("MEDIUM", 2, 3)); //can't be center of mass :/ value->addTerm(new fl::Ramp("HIGH", 2.5, 5)); value->setRange(0.0, 5.0); //use unarmed scouts if possible addRule("if strengthRatio is HIGH and heroStrength is LOW then Value is HIGH"); //we may want to use secondary hero(es) rather than main hero addRule("if strengthRatio is HIGH and heroStrength is MEDIUM then Value is MEDIUM"); addRule("if strengthRatio is HIGH and heroStrength is HIGH then Value is LOW"); //don't assign targets to heroes who are too weak, but prefer targets of our main hero (in case we need to gather army) addRule("if strengthRatio is LOW and heroStrength is LOW then Value is LOW"); //attempt to arm secondary heroes is not stupid addRule("if strengthRatio is LOW and heroStrength is MEDIUM then Value is HIGH"); addRule("if strengthRatio is LOW and heroStrength is HIGH then Value is LOW"); //do not cancel important goals addRule("if lockedMissionImportance is HIGH then Value is LOW"); addRule("if lockedMissionImportance is MEDIUM then Value is MEDIUM"); addRule("if lockedMissionImportance is LOW then Value is HIGH"); //pick nearby objects if it's easy, avoid long walks addRule("if turnDistance is SHORT then Value is HIGH"); addRule("if turnDistance is MEDIUM then Value is MEDIUM"); addRule("if turnDistance is LONG then Value is LOW"); } catch(fl::Exception & fe) { logAi->error("HeroMovementGoalEngineBase: %s", fe.getWhat()); } } void HeroMovementGoalEngineBase::setSharedFuzzyVariables(Goals::AbstractGoal & goal) { float turns = calculateTurnDistanceInputValue(goal); float missionImportanceData = 0; if(vstd::contains(ai->lockedHeroes, goal.hero)) { missionImportanceData = ai->lockedHeroes[goal.hero]->priority; } else if(goal.parent) { missionImportanceData = goal.parent->priority; } float strengthRatioData = 10.0f; //we are much stronger than enemy ui64 danger = evaluateDanger(goal.tile, goal.hero.h); if(danger) strengthRatioData = (fl::scalar)goal.hero.h->getTotalStrength() / danger; try { strengthRatio->setValue(strengthRatioData); heroStrength->setValue((fl::scalar)goal.hero->getTotalStrength() / ai->primaryHero()->getTotalStrength()); turnDistance->setValue(turns); missionImportance->setValue(missionImportanceData); } catch(fl::Exception & fe) { logAi->error("HeroMovementGoalEngineBase::setSharedFuzzyVariables: %s", fe.getWhat()); } } VisitObjEngine::VisitObjEngine() { try { objectValue = new fl::InputVariable("objectValue"); //value of that object type known by AI engine.addInputVariable(objectValue); //objectValue ranges are based on checking RMG priorities of some objects and checking LOW/MID/HIGH proportions for various values in QtFuzzyLite objectValue->addTerm(new fl::Ramp("LOW", 3500.0, 0.0)); objectValue->addTerm(new fl::Triangle("MEDIUM", 0.0, 8500.0)); std::vector multiRamp = { fl::Discrete::Pair(5000.0, 0.0), fl::Discrete::Pair(10000.0, 0.75), fl::Discrete::Pair(20000.0, 1.0) }; objectValue->addTerm(new fl::Discrete("HIGH", multiRamp)); objectValue->setRange(0.0, 20000.0); //relic artifact value is border value by design, even better things are scaled down. addRule("if objectValue is HIGH then Value is HIGH"); addRule("if objectValue is MEDIUM then Value is MEDIUM"); addRule("if objectValue is LOW then Value is LOW"); } catch(fl::Exception & fe) { logAi->error("FindWanderTarget: %s", fe.getWhat()); } configure(); } float VisitObjEngine::evaluate(Goals::VisitObj & goal) { if(!goal.hero) return 0; auto obj = ai->myCb->getObj(ObjectInstanceID(goal.objid)); if(!obj) { logAi->error("Goals::VisitObj objid " + std::to_string(goal.objid) + " no longer visible, probably goal used for something it's not intended"); return -100; // FIXME: Added check when goal was used for hero instead of VisitHero, but crashes are bad anyway } boost::optional objValueKnownByAI = MapObjectsEvaluator::getInstance().getObjectValue(obj); int objValue = 0; if(objValueKnownByAI != boost::none) //consider adding value manipulation based on object instances on map { objValue = std::min(std::max(objValueKnownByAI.get(), 0), 20000); } else { MapObjectsEvaluator::getInstance().addObjectData(obj->ID, obj->subID, 0); logGlobal->error("AI met object type it doesn't know - ID: " + std::to_string(obj->ID) + ", subID: " + std::to_string(obj->subID) + " - adding to database with value " + std::to_string(objValue)); } setSharedFuzzyVariables(goal); float output = -1.0f; try { objectValue->setValue(objValue); engine.process(); output = value->getValue(); } catch(fl::Exception & fe) { logAi->error("evaluate getWanderTargetObjectValue: %s", fe.getWhat()); } assert(output >= 0.0f); return output; } VisitTileEngine::VisitTileEngine() //so far no VisitTile-specific variables that are not shared with HeroMovementGoalEngineBase { configure(); } float VisitTileEngine::evaluate(Goals::VisitTile & goal) { //we assume that hero is already set and we want to choose most suitable one for the mission if(!goal.hero) return 0; //assert(cb->isInTheMap(g.tile)); setSharedFuzzyVariables(goal); try { engine.process(); goal.priority = value->getValue(); } catch(fl::Exception & fe) { logAi->error("evaluate VisitTile: %s", fe.getWhat()); } assert(goal.priority >= 0); return goal.priority; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/FuzzyEngines.h000066400000000000000000000037071342332007200210050ustar00rootroot00000000000000/* * FuzzyEngines.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include #include "Goals/AbstractGoal.h" class CArmedInstance; class engineBase //subclasses create fuzzylite variables with "new" that are not freed - this is desired as fl::Engine wants to destroy these... { protected: fl::Engine engine; fl::RuleBlock rules; virtual void configure(); void addRule(const std::string & txt); public: engineBase(); }; class TacticalAdvantageEngine : public engineBase { public: TacticalAdvantageEngine(); float getTacticalAdvantage(const CArmedInstance * we, const CArmedInstance * enemy); //returns factor how many times enemy is stronger than us private: fl::InputVariable * ourWalkers, *ourShooters, *ourFlyers, *enemyWalkers, *enemyShooters, *enemyFlyers; fl::InputVariable * ourSpeed, *enemySpeed; fl::InputVariable * bankPresent; fl::InputVariable * castleWalls; fl::OutputVariable * threat; }; class HeroMovementGoalEngineBase : public engineBase //in future - maybe derive from some (GoalEngineBase : public engineBase) class for handling non-movement goals with common utility for goal engines { public: HeroMovementGoalEngineBase(); protected: void setSharedFuzzyVariables(Goals::AbstractGoal & goal); fl::InputVariable * strengthRatio; fl::InputVariable * heroStrength; fl::InputVariable * turnDistance; fl::InputVariable * missionImportance; fl::OutputVariable * value; private: float calculateTurnDistanceInputValue(const Goals::AbstractGoal & goal) const; }; class VisitTileEngine : public HeroMovementGoalEngineBase { public: VisitTileEngine(); float evaluate(Goals::VisitTile & goal); }; class VisitObjEngine : public HeroMovementGoalEngineBase { public: VisitObjEngine(); float evaluate(Goals::VisitObj & goal); protected: fl::InputVariable * objectValue; }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/FuzzyHelper.cpp000066400000000000000000000110521342332007200211570ustar00rootroot00000000000000/* * FuzzyHelper.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "FuzzyHelper.h" #include "../../lib/mapObjects/CommonConstructors.h" #include "Goals/Goals.h" #include "VCAI.h" FuzzyHelper * fh; extern boost::thread_specific_ptr ai; Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec) { if(vec.empty()) { logAi->debug("FuzzyHelper found no goals. Returning Goals::Invalid."); //no possibilities found return sptr(Goals::Invalid()); } //a trick to switch between heroes less often - calculatePaths is costly auto sortByHeroes = [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool { return lhs->hero.h < rhs->hero.h; }; boost::sort(vec, sortByHeroes); for(auto g : vec) { setPriority(g); } auto compareGoals = [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool { return lhs->priority < rhs->priority; }; for(auto goal : vec) { logAi->trace("FuzzyHelper evaluated goal %s, priority=%.4f", goal->name(), goal->priority); } Goals::TSubgoal result = *boost::max_element(vec, compareGoals); logAi->debug("FuzzyHelper returned goal %s, priority=%.4f", result->name(), result->priority); return result; } ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) { //this one is not fuzzy anymore, just calculate weighted average auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); ui64 totalStrength = 0; ui8 totalChance = 0; for(auto config : bankInfo->getPossibleGuards()) { totalStrength += config.second.totalStrength * config.first; totalChance += config.first; } return totalStrength / std::max(totalChance, 1); //avoid division by zero } float FuzzyHelper::evaluate(Goals::VisitTile & g) { if(g.parent) { g.parent->accept(this); } return visitTileEngine.evaluate(g); } float FuzzyHelper::evaluate(Goals::BuildBoat & g) { const float buildBoatPenalty = 0.25; if(!g.parent) { return 0; } return g.parent->accept(this) - buildBoatPenalty; } float FuzzyHelper::evaluate(Goals::AdventureSpellCast & g) { if(!g.parent) { return 0; } const CSpell * spell = g.getSpell(); const float spellCastPenalty = (float)g.hero->getSpellCost(spell) / g.hero->mana; return g.parent->accept(this) - spellCastPenalty; } float FuzzyHelper::evaluate(Goals::CompleteQuest & g) { // TODO: How to evaluate quest complexity? const float questPenalty = 0.2; if(!g.parent) { return 0; } return g.parent->accept(this) * questPenalty; } float FuzzyHelper::evaluate(Goals::VisitObj & g) { if(g.parent) { g.parent->accept(this); } return visitObjEngine.evaluate(g); } float FuzzyHelper::evaluate(Goals::VisitHero & g) { auto obj = ai->myCb->getObj(ObjectInstanceID(g.objid)); //we assume for now that these goals are similar if(!obj) return -100; //hero died in the meantime else { g.setpriority(Goals::VisitTile(obj->visitablePos()).sethero(g.hero).accept(this)); } return g.priority; } float FuzzyHelper::evaluate(Goals::GatherArmy & g) { //the more army we need, the more important goal //the more army we lack, the less important goal float army = g.hero->getArmyStrength(); float ratio = g.value / std::max(g.value - army, 2000.0f); //2000 is about the value of hero recruited from tavern return 5 * (ratio / (ratio + 2)); //so 50% army gives 2.5, asymptotic 5 } float FuzzyHelper::evaluate(Goals::ClearWayTo & g) { if (!g.hero.h) return 0; //lowest priority return g.whatToDoToAchieve()->accept(this); } float FuzzyHelper::evaluate(Goals::BuildThis & g) { return g.priority; //TODO } float FuzzyHelper::evaluate(Goals::DigAtTile & g) { return 0; } float FuzzyHelper::evaluate(Goals::CollectRes & g) { return g.priority; //handled by ResourceManager } float FuzzyHelper::evaluate(Goals::Build & g) { return 0; } float FuzzyHelper::evaluate(Goals::BuyArmy & g) { return g.priority; } float FuzzyHelper::evaluate(Goals::Explore & g) { return 1; } float FuzzyHelper::evaluate(Goals::RecruitHero & g) { return 1; } float FuzzyHelper::evaluate(Goals::Invalid & g) { return -1e10; } float FuzzyHelper::evaluate(Goals::AbstractGoal & g) { logAi->warn("Cannot evaluate goal %s", g.name()); return g.priority; } void FuzzyHelper::setPriority(Goals::TSubgoal & g) //calls evaluate - Visitor pattern { g->setpriority(g->accept(this)); //this enforces returned value is set } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/FuzzyHelper.h000066400000000000000000000025511342332007200206300ustar00rootroot00000000000000/* * FuzzyHelper.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "FuzzyEngines.h" class CBank; class DLL_EXPORT FuzzyHelper { public: TacticalAdvantageEngine tacticalAdvantageEngine; VisitTileEngine visitTileEngine; VisitObjEngine visitObjEngine; float evaluate(Goals::Explore & g); float evaluate(Goals::RecruitHero & g); float evaluate(Goals::VisitTile & g); float evaluate(Goals::VisitObj & g); float evaluate(Goals::VisitHero & g); float evaluate(Goals::BuildThis & g); float evaluate(Goals::DigAtTile & g); float evaluate(Goals::CollectRes & g); float evaluate(Goals::Build & g); float evaluate(Goals::BuyArmy & g); float evaluate(Goals::BuildBoat & g); float evaluate(Goals::GatherArmy & g); float evaluate(Goals::ClearWayTo & g); float evaluate(Goals::CompleteQuest & g); float evaluate(Goals::AdventureSpellCast & g); float evaluate(Goals::Invalid & g); float evaluate(Goals::AbstractGoal & g); void setPriority(Goals::TSubgoal & g); ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class? Goals::TSubgoal chooseSolution(Goals::TGoalVec vec); //std::shared_ptr chooseSolution (std::vector> & vec); }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/000077500000000000000000000000001342332007200172325ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/AbstractGoal.cpp000066400000000000000000000101341342332007200223030ustar00rootroot00000000000000/* * AbstractGoal.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "AbstractGoal.h" #include "../VCAI.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; TSubgoal Goals::sptr(const AbstractGoal & tmp) { TSubgoal ptr; ptr.reset(tmp.clone()); return ptr; } std::string AbstractGoal::name() const //TODO: virtualize { std::string desc; switch(goalType) { case INVALID: return "INVALID"; case WIN: return "WIN"; case CONQUER: return "CONQUER"; case BUILD: return "BUILD"; case EXPLORE: desc = "EXPLORE"; break; case GATHER_ARMY: desc = "GATHER ARMY"; break; case BUY_ARMY: return "BUY ARMY"; break; case BOOST_HERO: desc = "BOOST_HERO (unsupported)"; break; case RECRUIT_HERO: return "RECRUIT HERO"; case BUILD_STRUCTURE: return "BUILD STRUCTURE"; case COLLECT_RES: desc = "COLLECT RESOURCE " + GameConstants::RESOURCE_NAMES[resID] + " (" + boost::lexical_cast(value) + ")"; break; case TRADE: { auto obj = cb->getObjInstance(ObjectInstanceID(objid)); if (obj) desc = (boost::format("TRADE %d of %s at %s") % value % GameConstants::RESOURCE_NAMES[resID] % obj->getObjectName()).str(); } break; case GATHER_TROOPS: desc = "GATHER TROOPS"; break; case VISIT_OBJ: { auto obj = cb->getObjInstance(ObjectInstanceID(objid)); if(obj) desc = "GET OBJ " + obj->getObjectName(); } break; case FIND_OBJ: desc = "FIND OBJ " + boost::lexical_cast(objid); break; case VISIT_HERO: { auto obj = cb->getObjInstance(ObjectInstanceID(objid)); if(obj) desc = "VISIT HERO " + obj->getObjectName(); } break; case GET_ART_TYPE: desc = "GET ARTIFACT OF TYPE " + VLC->arth->artifacts[aid]->Name(); break; case VISIT_TILE: desc = "VISIT TILE " + tile.toString(); break; case CLEAR_WAY_TO: desc = "CLEAR WAY TO " + tile.toString(); break; case DIG_AT_TILE: desc = "DIG AT TILE " + tile.toString(); break; default: return boost::lexical_cast(goalType); } if(hero.get(true)) //FIXME: used to crash when we lost hero and failed goal desc += " (" + hero->name + ")"; return desc; } bool AbstractGoal::operator==(const AbstractGoal & g) const { return false; } bool AbstractGoal::operator<(AbstractGoal & g) //for std::unique { //TODO: make sure it gets goals consistent with == operator if (goalType < g.goalType) return true; if (goalType > g.goalType) return false; if (hero < g.hero) return true; if (hero > g.hero) return false; if (tile < g.tile) return true; if (g.tile < tile) return false; if (objid < g.objid) return true; if (objid > g.objid) return false; if (town < g.town) return true; if (town > g.town) return false; if (value < g.value) return true; if (value > g.value) return false; if (priority < g.priority) return true; if (priority > g.priority) return false; if (resID < g.resID) return true; if (resID > g.resID) return false; if (bid < g.bid) return true; if (bid > g.bid) return false; if (aid < g.aid) return true; if (aid > g.aid) return false; return false; } //TODO: find out why the following are not generated automatically on MVS? bool TSubgoal::operator==(const TSubgoal & rhs) const { return *get() == *rhs.get(); //comparison for Goals is overloaded, so they don't need to be identical to match } bool TSubgoal::operator<(const TSubgoal & rhs) const { return get() < rhs.get(); //compae by value } bool AbstractGoal::invalid() const { return goalType == EGoals::INVALID; } void AbstractGoal::accept(VCAI * ai) { ai->tryRealize(*this); } float AbstractGoal::accept(FuzzyHelper * f) { return f->evaluate(*this); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/AbstractGoal.h000066400000000000000000000112151342332007200217510ustar00rootroot00000000000000/* * AbstractGoal.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../../lib/VCMI_Lib.h" #include "../../../lib/CBuildingHandler.h" #include "../../../lib/CCreatureHandler.h" #include "../../../lib/CTownHandler.h" #include "../AIUtility.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class AbstractGoal; class Explore; class RecruitHero; class VisitTile; class VisitObj; class VisitHero; class BuildThis; class DigAtTile; class CollectRes; class Build; class BuyArmy; class BuildBoat; class GatherArmy; class ClearWayTo; class Invalid; class Trade; class CompleteQuest; class AdventureSpellCast; enum EGoals { INVALID = -1, WIN, CONQUER, BUILD, //build needs to get a real reasoning EXPLORE, GATHER_ARMY, BOOST_HERO, RECRUIT_HERO, BUILD_STRUCTURE, //if hero set, then in visited town COLLECT_RES, GATHER_TROOPS, // val of creatures with objid VISIT_OBJ, //visit or defeat or collect the object FIND_OBJ, //find and visit any obj with objid + resid //TODO: consider universal subid for various types (aid, bid) VISIT_HERO, //heroes can move around - set goal abstract and track hero every turn GET_ART_TYPE, VISIT_TILE, //tile, in conjunction with hero elementar; assumes tile is reachable CLEAR_WAY_TO, DIG_AT_TILE,//elementar with hero on tile BUY_ARMY, //at specific town TRADE, //val resID at object objid BUILD_BOAT, COMPLETE_QUEST, ADVENTURE_SPELL_CAST }; class DLL_EXPORT TSubgoal : public std::shared_ptr { public: bool operator==(const TSubgoal & rhs) const; bool operator<(const TSubgoal & rhs) const; //TODO: serialize? }; typedef std::vector TGoalVec; //method chaining + clone pattern #define VSETTER(type, field) virtual AbstractGoal & set ## field(const type &rhs) {field = rhs; return *this;}; #define OSETTER(type, field) CGoal & set ## field(const type &rhs) override { field = rhs; return *this; }; #if 0 #define SETTER #endif // _DEBUG enum { LOW_PR = -1 }; DLL_EXPORT TSubgoal sptr(const AbstractGoal & tmp); struct DLL_EXPORT EvaluationContext { float movementCost; int manaCost; uint64_t danger; EvaluationContext() : movementCost(0.0), manaCost(0), danger(0) { } }; class DLL_EXPORT AbstractGoal { public: bool isElementar; VSETTER(bool, isElementar) bool isAbstract; VSETTER(bool, isAbstract) float priority; VSETTER(float, priority) int value; VSETTER(int, value) int resID; VSETTER(int, resID) int objid; VSETTER(int, objid) int aid; VSETTER(int, aid) int3 tile; VSETTER(int3, tile) HeroPtr hero; VSETTER(HeroPtr, hero) const CGTownInstance *town; VSETTER(CGTownInstance *, town) int bid; VSETTER(int, bid) TSubgoal parent; VSETTER(TSubgoal, parent) EvaluationContext evaluationContext; VSETTER(EvaluationContext, evaluationContext) AbstractGoal(EGoals goal = EGoals::INVALID) : goalType(goal), evaluationContext() { priority = 0; isElementar = false; isAbstract = false; value = 0; aid = -1; resID = -1; objid = -1; tile = int3(-1, -1, -1); town = nullptr; bid = -1; } virtual ~AbstractGoal() {} //FIXME: abstract goal should be abstract, but serializer fails to instantiate subgoals in such case virtual AbstractGoal * clone() const { return const_cast(this); } virtual TGoalVec getAllPossibleSubgoals() { return TGoalVec(); } virtual TSubgoal whatToDoToAchieve() { return sptr(AbstractGoal()); } EGoals goalType; virtual std::string name() const; virtual std::string completeMessage() const { return "This goal is unspecified!"; } bool invalid() const; ///Visitor pattern //TODO: make accept work for std::shared_ptr... somehow virtual void accept(VCAI * ai); //unhandled goal will report standard error virtual float accept(FuzzyHelper * f); virtual bool operator==(const AbstractGoal & g) const; bool operator<(AbstractGoal & g); //final virtual bool fulfillsMe(Goals::TSubgoal goal) //TODO: multimethod instead of type check { return false; //use this method to check if goal is fulfilled by another (not equal) goal, operator == is handled spearately } bool operator!=(const AbstractGoal & g) const { return !(*this == g); } template void serialize(Handler & h, const int version) { h & goalType; h & isElementar; h & isAbstract; h & priority; h & value; h & resID; h & objid; h & aid; h & tile; h & hero; h & town; h & bid; } }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/AdventureSpellCast.cpp000066400000000000000000000037511342332007200235140ustar00rootroot00000000000000/* * AdventureSpellCast.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "AdventureSpellCast.h" #include "../VCAI.h" #include "../FuzzyHelper.h" #include "../AIhelper.h" #include "../../lib/mapping/CMap.h" //for victory conditions #include "../../lib/CPathfinder.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const { return hero.h == other.hero.h; } TSubgoal AdventureSpellCast::whatToDoToAchieve() { if(!hero.validAndSet()) throw cannotFulfillGoalException("Invalid hero!"); auto spell = getSpell(); logAi->trace("Decomposing adventure spell cast of %s for hero %s", spell->name, hero->name); if(!spell->isAdventureSpell()) throw cannotFulfillGoalException(spell->name + " is not an adventure spell."); if(!hero->canCastThisSpell(spell)) throw cannotFulfillGoalException("Hero can not cast " + spell->name); if(hero->mana < hero->getSpellCost(spell)) throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->name); return iAmElementar(); } void AdventureSpellCast::accept(VCAI * ai) { if(town && spellID == SpellID::TOWN_PORTAL) { ai->selectedObject = town->id; } auto wait = cb->waitTillRealize; cb->waitTillRealize = true; cb->castSpell(hero.h, spellID, tile); ai->ah->resetPaths(); if(town && spellID == SpellID::TOWN_PORTAL) { // visit town ai->moveHeroToTile(town->visitablePos(), hero); } cb->waitTillRealize = wait; throw goalFulfilledException(sptr(*this)); } std::string AdventureSpellCast::name() const { return "AdventureSpellCast " + spellID.toSpell()->name; } std::string AdventureSpellCast::completeMessage() const { return "Spell casted successfully " + spellID.toSpell()->name; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/AdventureSpellCast.h000066400000000000000000000016311342332007200231540ustar00rootroot00000000000000/* * AdventureSpellCast.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" namespace Goals { class DLL_EXPORT AdventureSpellCast : public CGoal { private: SpellID spellID; public: AdventureSpellCast(HeroPtr hero, SpellID spellID) : CGoal(Goals::ADVENTURE_SPELL_CAST), spellID(spellID) { sethero(hero); } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } const CSpell * getSpell() const { return spellID.toSpell(); } TSubgoal whatToDoToAchieve() override; void accept(VCAI * ai) override; std::string name() const override; std::string completeMessage() const override; virtual bool operator==(const AdventureSpellCast & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Build.cpp000066400000000000000000000052761342332007200210070ustar00rootroot00000000000000/* * Build.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Build.h" #include "BuildThis.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; TGoalVec Build::getAllPossibleSubgoals() { TGoalVec ret; for(const CGTownInstance * t : cb->getTownsInfo()) { //start fresh with every town ai->ah->getBuildingOptions(t); auto immediateBuilding = ai->ah->immediateBuilding(); auto expensiveBuilding = ai->ah->expensiveBuilding(); //handling for early town development to save money and focus on income if(!t->hasBuilt(ai->ah->getMaxPossibleGoldBuilding(t)) && expensiveBuilding.is_initialized()) { auto potentialBuilding = expensiveBuilding.get(); switch(expensiveBuilding.get().bid) { case BuildingID::TOWN_HALL: case BuildingID::CITY_HALL: case BuildingID::CAPITOL: case BuildingID::FORT: case BuildingID::CITADEL: case BuildingID::CASTLE: //If above buildings are next to be bought, but no money... do not buy anything else, try to gather resources for these. Simple but has to suffice for now. auto goal = ai->ah->whatToDo(potentialBuilding.price, sptr(BuildThis(potentialBuilding.bid, t).setpriority(2.25))); ret.push_back(goal); return ret; break; } } if(immediateBuilding.is_initialized()) { ret.push_back(sptr(BuildThis(immediateBuilding.get().bid, t).setpriority(2))); //prioritize buildings we can build quick } else //try build later { if(expensiveBuilding.is_initialized()) { auto potentialBuilding = expensiveBuilding.get(); //gather resources for any we can't afford auto goal = ai->ah->whatToDo(potentialBuilding.price, sptr(BuildThis(potentialBuilding.bid, t).setpriority(0.5))); ret.push_back(goal); } } } if(ret.empty()) throw cannotFulfillGoalException("BUILD has been realized as much as possible."); else return ret; } TSubgoal Build::whatToDoToAchieve() { return fh->chooseSolution(getAllPossibleSubgoals()); } bool Build::fulfillsMe(TSubgoal goal) { if(goal->goalType == BUILD || goal->goalType == BUILD_STRUCTURE) return (!town || town == goal->town); //building anything will do, in this town if set else return false; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Build.h000066400000000000000000000012161342332007200204420ustar00rootroot00000000000000/* * Build.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT Build : public CGoal { public: Build() : CGoal(Goals::BUILD) { priority = 1; } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; virtual bool operator==(const Build & other) const override { return true; } }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/BuildBoat.cpp000066400000000000000000000040661342332007200216110ustar00rootroot00000000000000/* * BuildBoat.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "BuildBoat.h" #include "../VCAI.h" #include "../FuzzyHelper.h" #include "../AIhelper.h" #include "../../lib/mapping/CMap.h" //for victory conditions #include "../../lib/CPathfinder.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool BuildBoat::operator==(const BuildBoat & other) const { return shipyard->o->id == other.shipyard->o->id; } TSubgoal BuildBoat::whatToDoToAchieve() { if(cb->getPlayerRelations(ai->playerID, shipyard->o->tempOwner) == PlayerRelations::ENEMIES) { return fh->chooseSolution(ai->ah->howToVisitObj(shipyard->o)); } if(shipyard->shipyardStatus() != IShipyard::GOOD) { throw cannotFulfillGoalException("Shipyard is busy."); } TResources boatCost; shipyard->getBoatCost(boatCost); return ai->ah->whatToDo(boatCost, this->iAmElementar()); } void BuildBoat::accept(VCAI * ai) { TResources boatCost; shipyard->getBoatCost(boatCost); if(!cb->getResourceAmount().canAfford(boatCost)) { throw cannotFulfillGoalException("Can not afford boat"); } if(cb->getPlayerRelations(ai->playerID, shipyard->o->tempOwner) == PlayerRelations::ENEMIES) { throw cannotFulfillGoalException("Can not build boat in enemy shipyard"); } if(shipyard->shipyardStatus() != IShipyard::GOOD) { throw cannotFulfillGoalException("Shipyard is busy."); } logAi->trace( "Building boat at shipyard %s located at %s, estimated boat position %s", shipyard->o->getObjectName(), shipyard->o->visitablePos().toString(), shipyard->bestLocation().toString()); cb->buildBoat(shipyard); throw goalFulfilledException(sptr(*this)); } std::string BuildBoat::name() const { return "BuildBoat"; } std::string BuildBoat::completeMessage() const { return "Boat have been built at " + shipyard->o->visitablePos().toString(); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/BuildBoat.h000066400000000000000000000014401342332007200212470ustar00rootroot00000000000000/* * BuildBoat.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" namespace Goals { class DLL_EXPORT BuildBoat : public CGoal { private: const IShipyard * shipyard; public: BuildBoat(const IShipyard * shipyard) : CGoal(Goals::BUILD_BOAT), shipyard(shipyard) { priority = 0; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override; void accept(VCAI * ai) override; std::string name() const override; std::string completeMessage() const override; virtual bool operator==(const BuildBoat & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/BuildThis.cpp000066400000000000000000000033211342332007200216240ustar00rootroot00000000000000/* * BuildThis.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "BuildThis.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool BuildThis::operator==(const BuildThis & other) const { return town == other.town && bid == other.bid; } TSubgoal BuildThis::whatToDoToAchieve() { auto b = BuildingID(bid); // find town if not set if(!town && hero) town = hero->visitedTown; if(!town) { for(const CGTownInstance * t : cb->getTownsInfo()) { switch(cb->canBuildStructure(town, b)) { case EBuildingState::ALLOWED: town = t; break; //TODO: look for prerequisites? this is not our reponsibility default: continue; } } } if(town) //we have specific town to build this { switch(cb->canBuildStructure(town, b)) { case EBuildingState::ALLOWED: case EBuildingState::NO_RESOURCES: { auto res = town->town->buildings.at(BuildingID(bid))->resources; return ai->ah->whatToDo(res, iAmElementar()); //realize immediately or gather resources } break; default: throw cannotFulfillGoalException("Not possible to build"); } } else throw cannotFulfillGoalException("Cannot find town to build this"); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/BuildThis.h000066400000000000000000000017041342332007200212740ustar00rootroot00000000000000/* * BuildThis.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT BuildThis : public CGoal { public: BuildThis() //should be private, but unit test uses it : CGoal(Goals::BUILD_STRUCTURE) { } BuildThis(BuildingID Bid, const CGTownInstance * tid) : CGoal(Goals::BUILD_STRUCTURE) { bid = Bid; town = tid; priority = 1; } BuildThis(BuildingID Bid) : CGoal(Goals::BUILD_STRUCTURE) { bid = Bid; priority = 1; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override; //bool fulfillsMe(TSubgoal goal) override; virtual bool operator==(const BuildThis & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/BuyArmy.cpp000066400000000000000000000022371342332007200213320ustar00rootroot00000000000000/* * BuyArmy.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "BuyArmy.h" #include "../FuzzyHelper.h" #include "../AIhelper.h" #include "../../lib/mapObjects/CGTownInstance.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool BuyArmy::operator==(const BuyArmy & other) const { return town == other.town && objid == other.objid; } bool BuyArmy::fulfillsMe(TSubgoal goal) { //if (hero && hero != goal->hero) // return false; return town == goal->town && goal->value >= value; //can always buy more army } TSubgoal BuyArmy::whatToDoToAchieve() { //TODO: calculate the actual cost of units instead TResources price; price[Res::GOLD] = value * 0.4f; //some approximate value return ai->ah->whatToDo(price, iAmElementar()); //buy right now or gather resources } std::string BuyArmy::completeMessage() const { return boost::format("Bought army of value %d in town of %s") % value, town->name; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/BuyArmy.h000066400000000000000000000015161342332007200207760ustar00rootroot00000000000000/* * BuyArmy.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT BuyArmy : public CGoal { private: BuyArmy() : CGoal(Goals::BUY_ARMY) { } public: BuyArmy(const CGTownInstance * Town, int val) : CGoal(Goals::BUY_ARMY) { town = Town; //where to buy this army value = val; //expressed in AI unit strength priority = 3;//TODO: evaluate? } bool fulfillsMe(TSubgoal goal) override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; virtual bool operator==(const BuyArmy & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/CGoal.h000066400000000000000000000036321342332007200203740ustar00rootroot00000000000000/* * CGoal.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "AbstractGoal.h" #include "../FuzzyHelper.h" #include "../VCAI.h" struct HeroPtr; class VCAI; namespace Goals { template class DLL_EXPORT CGoal : public AbstractGoal { public: CGoal(EGoals goal = INVALID) : AbstractGoal(goal) { priority = 0; isElementar = false; isAbstract = false; value = 0; aid = -1; objid = -1; resID = -1; tile = int3(-1, -1, -1); town = nullptr; } OSETTER(bool, isElementar) OSETTER(bool, isAbstract) OSETTER(float, priority) OSETTER(int, value) OSETTER(int, resID) OSETTER(int, objid) OSETTER(int, aid) OSETTER(int3, tile) OSETTER(HeroPtr, hero) OSETTER(CGTownInstance *, town) OSETTER(int, bid) void accept(VCAI * ai) override { ai->tryRealize(static_cast(*this)); //casting enforces template instantiation } float accept(FuzzyHelper * f) override { return f->evaluate(static_cast(*this)); //casting enforces template instantiation } CGoal * clone() const override { return new T(static_cast(*this)); //casting enforces template instantiation } TSubgoal iAmElementar() const { TSubgoal ptr; ptr.reset(clone()); ptr->setisElementar(true); return ptr; } template void serialize(Handler & h, const int version) { h & static_cast(*this); //h & goalType & isElementar & isAbstract & priority; //h & value & resID & objid & aid & tile & hero & town & bid; } virtual bool operator==(const AbstractGoal & g) const override { if(goalType != g.goalType) return false; return (*this) == (static_cast(g)); } virtual bool operator==(const T & other) const = 0; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/ClearWayTo.cpp000066400000000000000000000037621342332007200217600ustar00rootroot00000000000000/* * ClearWayTo.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "ClearWayTo.h" #include "Explore.h" #include "RecruitHero.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../FuzzyHelper.h" #include "../AIhelper.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool ClearWayTo::operator==(const ClearWayTo & other) const { return other.hero.h == hero.h && other.tile == tile; } TSubgoal ClearWayTo::whatToDoToAchieve() { assert(cb->isInTheMap(tile)); //set tile if(!cb->isVisible(tile)) { logAi->error("Clear way should be used with visible tiles!"); return sptr(Explore()); } return (fh->chooseSolution(getAllPossibleSubgoals())); } bool ClearWayTo::fulfillsMe(TSubgoal goal) { if (goal->goalType == VISIT_TILE) { if (!hero || hero == goal->hero) return tile == goal->tile; } return false; } TGoalVec ClearWayTo::getAllPossibleSubgoals() { TGoalVec ret; std::vector heroes; if(hero) heroes.push_back(hero.h); else heroes = cb->getHeroesInfo(); for(auto h : heroes) { //TODO: handle clearing way to allied heroes that are blocked //if ((hero && hero->visitablePos() == tile && hero == *h) || //we can't free the way ourselves // h->visitablePos() == tile) //we are already on that tile! what does it mean? // continue; //if our hero is trapped, make sure we request clearing the way from OUR perspective vstd::concatenate(ret, ai->ah->howToVisitTile(h, tile)); } if(ret.empty() && ai->canRecruitAnyHero()) ret.push_back(sptr(RecruitHero())); if(ret.empty()) { logAi->warn("There is no known way to clear the way to tile %s", tile.toString()); throw goalFulfilledException(sptr(ClearWayTo(tile))); //make sure asigned hero gets unlocked } return ret; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/ClearWayTo.h000066400000000000000000000015371342332007200214230ustar00rootroot00000000000000/* * ClearWayTo.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT ClearWayTo : public CGoal { public: ClearWayTo() : CGoal(Goals::CLEAR_WAY_TO) { } ClearWayTo(int3 Tile) : CGoal(Goals::CLEAR_WAY_TO) { tile = Tile; priority = 5; } ClearWayTo(int3 Tile, HeroPtr h) : CGoal(Goals::CLEAR_WAY_TO) { tile = Tile; hero = h; priority = 5; } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; virtual bool operator==(const ClearWayTo & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/CollectRes.cpp000066400000000000000000000121661342332007200220030ustar00rootroot00000000000000/* * CollectRes.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool CollectRes::operator==(const CollectRes & other) const { return resID == other.resID; } TGoalVec CollectRes::getAllPossibleSubgoals() { TGoalVec ret; auto givesResource = [this](const CGObjectInstance * obj) -> bool { //TODO: move this logic to object side //TODO: remember mithril exists //TODO: water objects //TODO: Creature banks //return false first from once-visitable, before checking if they were even visited switch (obj->ID.num) { case Obj::TREASURE_CHEST: return resID == Res::GOLD; break; case Obj::RESOURCE: return obj->subID == resID; break; case Obj::MINE: return (obj->subID == resID && (cb->getPlayerRelations(obj->tempOwner, ai->playerID) == PlayerRelations::ENEMIES)); //don't capture our mines break; case Obj::CAMPFIRE: return true; //contains all resources break; case Obj::WINDMILL: switch (resID) { case Res::GOLD: case Res::WOOD: return false; } break; case Obj::WATER_WHEEL: if (resID != Res::GOLD) return false; break; case Obj::MYSTICAL_GARDEN: if ((resID != Res::GOLD) && (resID != Res::GEMS)) return false; break; case Obj::LEAN_TO: case Obj::WAGON: if (resID != Res::GOLD) return false; break; default: return false; break; } return !vstd::contains(ai->alreadyVisited, obj); //for weekly / once visitable }; std::vector objs; for (auto obj : ai->visitableObjs) { if (givesResource(obj)) objs.push_back(obj); } for (auto h : cb->getHeroesInfo()) { std::vector ourObjs(objs); //copy common objects for (auto obj : ai->reservedHeroesMap[h]) //add objects reserved by this hero { if (givesResource(obj)) ourObjs.push_back(obj); } for (auto obj : ourObjs) { auto waysToGo = ai->ah->howToVisitObj(h, ObjectIdRef(obj)); vstd::concatenate(ret, waysToGo); } } return ret; } TSubgoal CollectRes::whatToDoToAchieve() { auto goals = getAllPossibleSubgoals(); auto trade = whatToDoToTrade(); if (!trade->invalid()) goals.push_back(trade); if (goals.empty()) return sptr(Explore()); //we can always do that else return fh->chooseSolution(goals); //TODO: evaluate trading } TSubgoal CollectRes::whatToDoToTrade() { std::vector markets; std::vector visObjs; ai->retrieveVisitableObjs(visObjs, true); for (const CGObjectInstance * obj : visObjs) { if (const IMarket * m = IMarket::castFrom(obj, false)) { if (obj->ID == Obj::TOWN && obj->tempOwner == ai->playerID && m->allowsTrade(EMarketMode::RESOURCE_RESOURCE)) markets.push_back(m); else if (obj->ID == Obj::TRADING_POST) markets.push_back(m); } } boost::sort(markets, [](const IMarket * m1, const IMarket * m2) -> bool { return m1->getMarketEfficiency() < m2->getMarketEfficiency(); }); markets.erase(boost::remove_if(markets, [](const IMarket * market) -> bool { if (!(market->o->ID == Obj::TOWN && market->o->tempOwner == ai->playerID)) { if (!ai->isAccessible(market->o->visitablePos())) return true; } return false; }), markets.end()); if (!markets.size()) { for (const CGTownInstance * t : cb->getTownsInfo()) { if (cb->canBuildStructure(t, BuildingID::MARKETPLACE) == EBuildingState::ALLOWED) return sptr(BuildThis(BuildingID::MARKETPLACE, t).setpriority(2)); } } else { const IMarket * m = markets.back(); //attempt trade at back (best prices) int howManyCanWeBuy = 0; for (Res::ERes i = Res::WOOD; i <= Res::GOLD; vstd::advance(i, 1)) { if (i == resID) continue; int toGive = -1, toReceive = -1; m->getOffer(i, resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE); assert(toGive > 0 && toReceive > 0); howManyCanWeBuy += toReceive * (ai->ah->freeResources()[i] / toGive); } if (howManyCanWeBuy >= value) { auto backObj = cb->getTopObj(m->o->visitablePos()); //it'll be a hero if we have one there; otherwise marketplace assert(backObj); auto objid = m->o->id.getNum(); if (backObj->tempOwner != ai->playerID) //top object not owned { return sptr(VisitObj(objid)); //just go there } else //either it's our town, or we have hero there { return sptr(Trade(resID, value, objid).setisElementar(true)); //we can do this immediately } } } return sptr(Invalid()); //cannot trade } bool CollectRes::fulfillsMe(TSubgoal goal) { if (goal->resID == resID) if (goal->value >= value) return true; return false; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/CollectRes.h000066400000000000000000000014471342332007200214500ustar00rootroot00000000000000/* * CollectRes.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT CollectRes : public CGoal { public: CollectRes() : CGoal(Goals::COLLECT_RES) { } CollectRes(int rid, int val) : CGoal(Goals::COLLECT_RES) { resID = rid; value = val; priority = 2; } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; TSubgoal whatToDoToTrade(); bool fulfillsMe(TSubgoal goal) override; //TODO: Trade virtual bool operator==(const CollectRes & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/CompleteQuest.cpp000066400000000000000000000133061342332007200225330ustar00rootroot00000000000000/* * CompleteQuest.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../FuzzyHelper.h" #include "../AIhelper.h" #include "../../lib/mapping/CMap.h" //for victory conditions #include "../../lib/CPathfinder.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool CompleteQuest::operator==(const CompleteQuest & other) const { return q.quest->qid == other.q.quest->qid; } TGoalVec CompleteQuest::getAllPossibleSubgoals() { TGoalVec solutions; if(q.quest->missionType && q.quest->progress != CQuest::COMPLETE) { logAi->debug("Trying to realize quest: %s", questToString()); switch(q.quest->missionType) { case CQuest::MISSION_ART: return missionArt(); case CQuest::MISSION_HERO: return missionHero(); case CQuest::MISSION_ARMY: return missionArmy(); case CQuest::MISSION_RESOURCES: return missionResources(); case CQuest::MISSION_KILL_HERO: case CQuest::MISSION_KILL_CREATURE: return missionDestroyObj(); case CQuest::MISSION_PRIMARY_STAT: return missionIncreasePrimaryStat(); case CQuest::MISSION_LEVEL: return missionLevel(); case CQuest::MISSION_PLAYER: if(ai->playerID.getNum() != q.quest->m13489val) logAi->debug("Can't be player of color %d", q.quest->m13489val); break; case CQuest::MISSION_KEYMASTER: return missionKeymaster(); } //end of switch } return TGoalVec(); } TSubgoal CompleteQuest::whatToDoToAchieve() { if(q.quest->missionType == CQuest::MISSION_NONE) { throw cannotFulfillGoalException("Can not complete inactive quest"); } TGoalVec solutions = getAllPossibleSubgoals(); if(solutions.empty()) throw cannotFulfillGoalException("Can not complete quest " + questToString()); TSubgoal result = fh->chooseSolution(solutions); logAi->trace( "Returning %s, tile: %s, objid: %d, hero: %s", result->name(), result->tile.toString(), result->objid, result->hero.validAndSet() ? result->hero->name : "not specified"); return result; } std::string CompleteQuest::name() const { return "CompleteQuest"; } std::string CompleteQuest::completeMessage() const { return "Completed quest " + questToString(); } std::string CompleteQuest::questToString() const { if(q.quest->missionType == CQuest::MISSION_NONE) return "inactive quest"; MetaString ms; q.quest->getRolloverText(ms, false); return ms.toString(); } TGoalVec CompleteQuest::tryCompleteQuest() const { TGoalVec solutions; auto heroes = cb->getHeroesInfo(); //TODO: choose best / free hero from among many possibilities? for(auto hero : heroes) { if(q.quest->checkQuest(hero)) { vstd::concatenate(solutions, ai->ah->howToVisitObj(hero, ObjectIdRef(q.obj->id))); } } return solutions; } TGoalVec CompleteQuest::missionArt() const { TGoalVec solutions = tryCompleteQuest(); if(!solutions.empty()) return solutions; for(auto art : q.quest->m5arts) { solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport? } return solutions; } TGoalVec CompleteQuest::missionHero() const { TGoalVec solutions = tryCompleteQuest(); if(solutions.empty()) { //rule of a thumb - quest heroes usually are locked in prisons solutions.push_back(sptr(FindObj(Obj::PRISON))); } return solutions; } TGoalVec CompleteQuest::missionArmy() const { TGoalVec solutions = tryCompleteQuest(); if(!solutions.empty()) return solutions; for(auto creature : q.quest->m6creatures) { solutions.push_back(sptr(GatherTroops(creature.type->idNumber, creature.count))); } return solutions; } TGoalVec CompleteQuest::missionIncreasePrimaryStat() const { TGoalVec solutions = tryCompleteQuest(); if(solutions.empty()) { for(int i = 0; i < q.quest->m2stats.size(); ++i) { // TODO: library, school and other boost objects logAi->debug("Don't know how to increase primary stat %d", i); } } return solutions; } TGoalVec CompleteQuest::missionLevel() const { TGoalVec solutions = tryCompleteQuest(); if(solutions.empty()) { logAi->debug("Don't know how to reach hero level %d", q.quest->m13489val); } return solutions; } TGoalVec CompleteQuest::missionKeymaster() const { TGoalVec solutions = tryCompleteQuest(); if(solutions.empty()) { solutions.push_back(sptr(Goals::FindObj(Obj::KEYMASTER, q.obj->subID))); } return solutions; } TGoalVec CompleteQuest::missionResources() const { TGoalVec solutions; auto heroes = cb->getHeroesInfo(); //TODO: choose best / free hero from among many possibilities? if(heroes.size()) { if(q.quest->checkQuest(heroes.front())) //it doesn't matter which hero it is { return ai->ah->howToVisitObj(q.obj); } else { for(int i = 0; i < q.quest->m7resources.size(); ++i) { if(q.quest->m7resources[i]) solutions.push_back(sptr(CollectRes(i, q.quest->m7resources[i]))); } } } else { solutions.push_back(sptr(Goals::RecruitHero())); //FIXME: checkQuest requires any hero belonging to player :( } return solutions; } TGoalVec CompleteQuest::missionDestroyObj() const { TGoalVec solutions; auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); if(!obj) return ai->ah->howToVisitObj(q.obj); if(obj->ID == Obj::HERO) { auto relations = cb->getPlayerRelations(ai->playerID, obj->tempOwner); if(relations == PlayerRelations::SAME_PLAYER) { auto heroToProtect = cb->getHero(obj->id); solutions.push_back(sptr(GatherArmy().sethero(heroToProtect))); } else if(relations == PlayerRelations::ENEMIES) { solutions = ai->ah->howToVisitObj(obj); } } return solutions; }vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/CompleteQuest.h000066400000000000000000000021471342332007200222010ustar00rootroot00000000000000/* * CompleteQuest.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" #include "../../../lib/VCMI_Lib.h" namespace Goals { class DLL_EXPORT CompleteQuest : public CGoal { private: const QuestInfo q; public: CompleteQuest(const QuestInfo quest) : CGoal(Goals::COMPLETE_QUEST), q(quest) { } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string name() const override; std::string completeMessage() const override; virtual bool operator==(const CompleteQuest & other) const override; private: TGoalVec tryCompleteQuest() const; TGoalVec missionArt() const; TGoalVec missionHero() const; TGoalVec missionArmy() const; TGoalVec missionResources() const; TGoalVec missionDestroyObj() const; TGoalVec missionIncreasePrimaryStat() const; TGoalVec missionLevel() const; TGoalVec missionKeymaster() const; std::string questToString() const; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Conquer.cpp000066400000000000000000000041261342332007200213550ustar00rootroot00000000000000/* * Conquer.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool Conquer::operator==(const Conquer & other) const { return other.hero.h == hero.h; } TSubgoal Conquer::whatToDoToAchieve() { logAi->trace("Entering goal CONQUER"); return fh->chooseSolution(getAllPossibleSubgoals()); } TGoalVec Conquer::getAllPossibleSubgoals() { TGoalVec ret; auto conquerable = [](const CGObjectInstance * obj) -> bool { if(cb->getPlayerRelations(ai->playerID, obj->tempOwner) == PlayerRelations::ENEMIES) { switch(obj->ID.num) { case Obj::TOWN: case Obj::HERO: case Obj::CREATURE_GENERATOR1: case Obj::MINE: //TODO: check ai->knownSubterraneanGates return true; } } return false; }; std::vector objs; for(auto obj : ai->visitableObjs) { if(conquerable(obj)) objs.push_back(obj); } for(auto h : cb->getHeroesInfo()) { std::vector ourObjs(objs); //copy common objects for(auto obj : ai->reservedHeroesMap[h]) //add objects reserved by this hero { if(conquerable(obj)) ourObjs.push_back(obj); } for(auto obj : ourObjs) { auto waysToGo = ai->ah->howToVisitObj(h, ObjectIdRef(obj)); vstd::concatenate(ret, waysToGo); } } if(!objs.empty() && ai->canRecruitAnyHero()) //probably no point to recruit hero if we see no objects to capture ret.push_back(sptr(RecruitHero())); if(ret.empty()) ret.push_back(sptr(Explore())); //we need to find an enemy return ret; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Conquer.h000066400000000000000000000011301342332007200210120ustar00rootroot00000000000000/* * Conquer.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT Conquer : public CGoal { public: Conquer() : CGoal(Goals::CONQUER) { priority = 10; } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; virtual bool operator==(const Conquer & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/DigAtTile.cpp000066400000000000000000000017471342332007200215550ustar00rootroot00000000000000/* * DigAtTile.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "DigAtTile.h" #include "VisitTile.h" #include "../VCAI.h" #include "../AIUtility.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool DigAtTile::operator==(const DigAtTile & other) const { return other.hero.h == hero.h && other.tile == tile; } TSubgoal DigAtTile::whatToDoToAchieve() { const CGObjectInstance * firstObj = vstd::frontOrNull(cb->getVisitableObjs(tile)); if(firstObj && firstObj->ID == Obj::HERO && firstObj->tempOwner == ai->playerID) //we have hero at dest { const CGHeroInstance * h = dynamic_cast(firstObj); sethero(h).setisElementar(true); return sptr(*this); } return sptr(VisitTile(tile)); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/DigAtTile.h000066400000000000000000000013611342332007200212120ustar00rootroot00000000000000/* * DigAtTile.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT DigAtTile : public CGoal //elementar with hero on tile { public: DigAtTile() : CGoal(Goals::DIG_AT_TILE) { } DigAtTile(int3 Tile) : CGoal(Goals::DIG_AT_TILE) { tile = Tile; priority = 20; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override; virtual bool operator==(const DigAtTile & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Explore.cpp000066400000000000000000000236151342332007200213630ustar00rootroot00000000000000/* * Explore.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" #include "../../../lib/CPlayerState.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; namespace Goals { struct ExplorationHelper { HeroPtr hero; int sightRadius; float bestValue; TSubgoal bestGoal; VCAI * aip; CCallback * cbp; const TeamState * ts; int3 ourPos; bool allowDeadEndCancellation; bool allowGatherArmy; ExplorationHelper(HeroPtr h, bool gatherArmy) { cbp = cb.get(); aip = ai.get(); hero = h; ts = cbp->getPlayerTeam(ai->playerID); sightRadius = hero->getSightRadius(); bestGoal = sptr(Goals::Invalid()); bestValue = 0; ourPos = h->convertPosition(h->pos, false); allowDeadEndCancellation = true; allowGatherArmy = gatherArmy; } void scanSector(int scanRadius) { for(int x = ourPos.x - scanRadius; x <= ourPos.x + scanRadius; x++) { for(int y = ourPos.y - scanRadius; y <= ourPos.y + scanRadius; y++) { int3 tile = int3(x, y, ourPos.z); if(cbp->isInTheMap(tile) && ts->fogOfWarMap[tile.x][tile.y][tile.z]) { scanTile(tile); } } } } void scanMap() { int3 mapSize = cbp->getMapSize(); int perimeter = 2 * sightRadius * (mapSize.x + mapSize.y); std::vector from; std::vector to; from.reserve(perimeter); to.reserve(perimeter); foreach_tile_pos([&](const int3 & pos) { if(ts->fogOfWarMap[pos.x][pos.y][pos.z]) { bool hasInvisibleNeighbor = false; foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour) { if(!ts->fogOfWarMap[neighbour.x][neighbour.y][neighbour.z]) { hasInvisibleNeighbor = true; } }); if(hasInvisibleNeighbor) from.push_back(pos); } }); logAi->debug("Exploration scan visible area perimeter for hero %s", hero.name); for(const int3 & tile : from) { scanTile(tile); } if(!bestGoal->invalid()) { return; } allowDeadEndCancellation = false; for(int i = 0; i < sightRadius; i++) { getVisibleNeighbours(from, to); vstd::concatenate(from, to); vstd::removeDuplicates(from); } logAi->debug("Exploration scan all possible tiles for hero %s", hero.name); for(const int3 & tile : from) { scanTile(tile); } } void scanTile(const int3 & tile) { if(tile == ourPos || !aip->ah->isTileAccessible(hero, tile)) //shouldn't happen, but it does return; int tilesDiscovered = howManyTilesWillBeDiscovered(tile); if(!tilesDiscovered) return; auto waysToVisit = aip->ah->howToVisitTile(hero, tile, allowGatherArmy); for(auto goal : waysToVisit) { if(goal->evaluationContext.movementCost <= 0.0) // should not happen continue; float ourValue = (float)tilesDiscovered * tilesDiscovered / goal->evaluationContext.movementCost; if(ourValue > bestValue) //avoid costly checks of tiles that don't reveal much { auto obj = cb->getTopObj(tile); // picking up resources does not yield any exploration at all. // if it blocks the way to some explorable tile AIPathfinder will take care of it if(obj && obj->blockVisit) { continue; } if(isSafeToVisit(hero, tile)) { bestGoal = goal; bestValue = ourValue; } } } } void getVisibleNeighbours(const std::vector & tiles, std::vector & out) const { for(const int3 & tile : tiles) { foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour) { if(ts->fogOfWarMap[neighbour.x][neighbour.y][neighbour.z]) { out.push_back(neighbour); } }); } } int howManyTilesWillBeDiscovered( const int3 & pos) const { int ret = 0; for(int x = pos.x - sightRadius; x <= pos.x + sightRadius; x++) { for(int y = pos.y - sightRadius; y <= pos.y + sightRadius; y++) { int3 npos = int3(x, y, pos.z); if(cbp->isInTheMap(npos) && pos.dist2d(npos) - 0.5 < sightRadius && !ts->fogOfWarMap[npos.x][npos.y][npos.z]) { if(allowDeadEndCancellation && !hasReachableNeighbor(npos)) { continue; } ret++; } } } return ret; } bool hasReachableNeighbor(const int3 &pos) const { for(crint3 dir : int3::getDirs()) { int3 tile = pos + dir; if(cbp->isInTheMap(tile)) { auto isAccessible = aip->ah->isTileAccessible(hero, tile); if(isAccessible) return true; } } return false; } }; } bool Explore::operator==(const Explore & other) const { return other.hero.h == hero.h && other.allowGatherArmy == allowGatherArmy; } std::string Explore::completeMessage() const { return "Hero " + hero.get()->name + " completed exploration"; } TSubgoal Explore::whatToDoToAchieve() { return fh->chooseSolution(getAllPossibleSubgoals()); } TGoalVec Explore::getAllPossibleSubgoals() { TGoalVec ret; std::vector heroes; if(hero) { heroes.push_back(hero.h); } else { //heroes = ai->getUnblockedHeroes(); heroes = cb->getHeroesInfo(); vstd::erase_if(heroes, [](const HeroPtr h) { if(ai->getGoal(h)->goalType == EXPLORE) //do not reassign hero who is already explorer return true; if(!ai->isAbleToExplore(h)) return true; return !h->movement; //saves time, immobile heroes are useless anyway }); } //try to use buildings that uncover map std::vector objs; for(auto obj : ai->visitableObjs) { if(!vstd::contains(ai->alreadyVisited, obj)) { switch(obj->ID.num) { case Obj::REDWOOD_OBSERVATORY: case Obj::PILLAR_OF_FIRE: case Obj::CARTOGRAPHER: objs.push_back(obj); break; case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_TWO_WAY: case Obj::SUBTERRANEAN_GATE: auto tObj = dynamic_cast(obj); assert(ai->knownTeleportChannels.find(tObj->channel) != ai->knownTeleportChannels.end()); if(TeleportChannel::IMPASSABLE != ai->knownTeleportChannels[tObj->channel]->passability) objs.push_back(obj); break; } } else { switch(obj->ID.num) { case Obj::MONOLITH_TWO_WAY: case Obj::SUBTERRANEAN_GATE: auto tObj = dynamic_cast(obj); if(TeleportChannel::IMPASSABLE == ai->knownTeleportChannels[tObj->channel]->passability) break; for(auto exit : ai->knownTeleportChannels[tObj->channel]->exits) { if(!cb->getObj(exit)) { // Always attempt to visit two-way teleports if one of channel exits is not visible objs.push_back(obj); break; } } break; } } } for(auto h : heroes) { for(auto obj : objs) //double loop, performance risk? { auto waysToVisitObj = ai->ah->howToVisitObj(h, obj, allowGatherArmy); vstd::concatenate(ret, waysToVisitObj); } TSubgoal goal = exploreNearestNeighbour(h); if(!goal->invalid()) { ret.push_back(goal); } } if(ret.empty()) { for(auto h : heroes) { logAi->trace("Exploration searching for a new point for hero %s", h->name); TSubgoal goal = explorationNewPoint(h); if(goal->invalid()) { ai->markHeroUnableToExplore(h); //there is no freely accessible tile, do not poll this hero anymore } else { ret.push_back(goal); } } } //we either don't have hero yet or none of heroes can explore if((!hero || ret.empty()) && ai->canRecruitAnyHero()) ret.push_back(sptr(RecruitHero())); if(ret.empty()) { throw goalFulfilledException(sptr(Explore().sethero(hero))); } return ret; } bool Explore::fulfillsMe(TSubgoal goal) { if(goal->goalType == EXPLORE) { if(goal->hero) return hero == goal->hero; else return true; //cancel ALL exploration } return false; } TSubgoal Explore::explorationBestNeighbour(int3 hpos, HeroPtr h) const { ExplorationHelper scanResult(h, allowGatherArmy); for(crint3 dir : int3::getDirs()) { int3 tile = hpos + dir; if(cb->isInTheMap(tile)) { scanResult.scanTile(tile); } } return scanResult.bestGoal; } TSubgoal Explore::explorationNewPoint(HeroPtr h) const { ExplorationHelper scanResult(h, allowGatherArmy); scanResult.scanSector(10); if(!scanResult.bestGoal->invalid()) { return scanResult.bestGoal; } scanResult.scanMap(); return scanResult.bestGoal; } TSubgoal Explore::exploreNearestNeighbour(HeroPtr h) const { TimeCheck tc("where to explore"); int3 hpos = h->visitablePos(); //look for nearby objs -> visit them if they're close enough const int DIST_LIMIT = 3; const float COST_LIMIT = .2; //todo: fine tune std::vector nearbyVisitableObjs; for(int x = hpos.x - DIST_LIMIT; x <= hpos.x + DIST_LIMIT; ++x) //get only local objects instead of all possible objects on the map { for(int y = hpos.y - DIST_LIMIT; y <= hpos.y + DIST_LIMIT; ++y) { for(auto obj : cb->getVisitableObjs(int3(x, y, hpos.z), false)) { if(ai->isGoodForVisit(obj, h, COST_LIMIT)) { nearbyVisitableObjs.push_back(obj); } } } } if(nearbyVisitableObjs.size()) { vstd::removeDuplicates(nearbyVisitableObjs); //one object may occupy multiple tiles boost::sort(nearbyVisitableObjs, CDistanceSorter(h.get())); TSubgoal pickupNearestObj = fh->chooseSolution(ai->ah->howToVisitObj(h, nearbyVisitableObjs.back(), false)); if(!pickupNearestObj->invalid()) { return pickupNearestObj; } } //check if nearby tiles allow us to reveal anything - this is quick return explorationBestNeighbour(hpos, h); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Explore.h000066400000000000000000000027741342332007200210330ustar00rootroot00000000000000/* * Explore.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { struct ExplorationHelper; class DLL_EXPORT Explore : public CGoal { private: bool allowGatherArmy; public: Explore(bool allowGatherArmy) : CGoal(Goals::EXPLORE), allowGatherArmy(allowGatherArmy) { priority = 1; } Explore() : Explore(true) { } Explore(HeroPtr h) : CGoal(Goals::EXPLORE) { hero = h; priority = 1; } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; bool fulfillsMe(TSubgoal goal) override; virtual bool operator==(const Explore & other) const override; private: TSubgoal exploreNearestNeighbour(HeroPtr h) const; TSubgoal explorationNewPoint(HeroPtr h) const; TSubgoal explorationBestNeighbour(int3 hpos, HeroPtr h) const; void explorationScanTile(const int3 & tile, ExplorationHelper & scanResult) const; bool hasReachableNeighbor(const int3 &pos, HeroPtr hero, CCallback * cbp, VCAI * vcai) const; void getVisibleNeighbours( const std::vector & tiles, std::vector & out, CCallback * cbp, const TeamState * ts) const; int howManyTilesWillBeDiscovered(const int3 & pos, ExplorationHelper & scanResult) const; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/FindObj.cpp000066400000000000000000000032741342332007200212570ustar00rootroot00000000000000/* * FindObj.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "FindObj.h" #include "VisitObj.h" #include "Explore.h" #include "../VCAI.h" #include "../AIUtility.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool FindObj::operator==(const FindObj & other) const { return other.hero.h == hero.h && other.objid == objid; } TSubgoal FindObj::whatToDoToAchieve() { const CGObjectInstance * o = nullptr; if(resID > -1) //specified { for(const CGObjectInstance * obj : ai->visitableObjs) { if(obj->ID == objid && obj->subID == resID) { o = obj; break; //TODO: consider multiple objects and choose best } } } else { for(const CGObjectInstance * obj : ai->visitableObjs) { if(obj->ID == objid) { o = obj; break; //TODO: consider multiple objects and choose best } } } if(o && ai->isAccessible(o->pos)) //we don't use isAccessibleForHero as we don't know which hero it is return sptr(VisitObj(o->id.getNum())); else return sptr(Explore()); } bool FindObj::fulfillsMe(TSubgoal goal) { if (goal->goalType == VISIT_TILE) //visiting tile visits object at same time { if (!hero || hero == goal->hero) for (auto obj : cb->getVisitableObjs(goal->tile)) //check if any object on that tile matches criteria if (obj->visitablePos() == goal->tile) //object could be removed if (obj->ID == objid && obj->subID == resID) //same type and subtype return true; } return false; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/FindObj.h000066400000000000000000000015731342332007200207240ustar00rootroot00000000000000/* * FindObj.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT FindObj : public CGoal { public: FindObj() {} // empty constructor not allowed FindObj(int ID) : CGoal(Goals::FIND_OBJ) { objid = ID; resID = -1; //subid unspecified priority = 1; } FindObj(int ID, int subID) : CGoal(Goals::FIND_OBJ) { objid = ID; resID = subID; priority = 1; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; virtual bool operator==(const FindObj & other) const override; }; }vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/GatherArmy.cpp000066400000000000000000000132361342332007200220060ustar00rootroot00000000000000/* * GatherArmy.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool GatherArmy::operator==(const GatherArmy & other) const { return other.hero.h == hero.h || town == other.town; } std::string GatherArmy::completeMessage() const { return "Hero " + hero.get()->name + " gathered army of value " + boost::lexical_cast(value); } TSubgoal GatherArmy::whatToDoToAchieve() { //TODO: find hero if none set assert(hero.h); return fh->chooseSolution(getAllPossibleSubgoals()); //find dwelling. use current hero to prevent him from doing nothing. } TGoalVec GatherArmy::getAllPossibleSubgoals() { //get all possible towns, heroes and dwellings we may use TGoalVec ret; if(!hero.validAndSet()) { return ret; } //TODO: include evaluation of monsters gather in calculation for(auto t : cb->getTownsInfo()) { auto waysToVisit = ai->ah->howToVisitObj(hero, t); if(waysToVisit.size()) { //grab army from town if(!t->visitingHero && howManyReinforcementsCanGet(hero.get(), t)) { if(!vstd::contains(ai->townVisitsThisWeek[hero], t)) vstd::concatenate(ret, waysToVisit); } //buy army in town if (!t->visitingHero || t->visitingHero == hero.get(true)) { std::vector values = { value, (int)howManyReinforcementsCanBuy(t->getUpperArmy(), t), (int)howManyReinforcementsCanBuy(hero.get(), t) }; int val = *std::min_element(values.begin(), values.end()); if (val) { auto goal = sptr(BuyArmy(t, val).sethero(hero)); if(!ai->ah->containsObjective(goal)) //avoid loops caused by reserving same objective twice ret.push_back(goal); else logAi->debug("Can not buy army, because of ai->ah->containsObjective"); } } //build dwelling //TODO: plan building over multiple turns? //auto bid = ah->canBuildAnyStructure(t, std::vector(unitsSource, unitsSource + ARRAY_COUNT(unitsSource)), 8 - cb->getDate(Date::DAY_OF_WEEK)); //Do not use below code for now, rely on generic Build. Code below needs to know a lot of town/resource context to do more good than harm /*auto bid = ai->ah->canBuildAnyStructure(t, std::vector(unitsSource, unitsSource + ARRAY_COUNT(unitsSource)), 1); if (bid.is_initialized()) { auto goal = sptr(BuildThis(bid.get(), t).setpriority(priority)); if (!ai->ah->containsObjective(goal)) //avoid loops caused by reserving same objective twice ret.push_back(goal); else logAi->debug("Can not build a structure, because of ai->ah->containsObjective"); }*/ } } auto otherHeroes = cb->getHeroesInfo(); auto heroDummy = hero; vstd::erase_if(otherHeroes, [heroDummy](const CGHeroInstance * h) { if(h == heroDummy.h) return true; else if(!ai->isAccessibleForHero(heroDummy->visitablePos(), h, true)) return true; else if(!ai->canGetArmy(heroDummy.h, h)) //TODO: return actual aiValue return true; else if(ai->getGoal(h)->goalType == GATHER_ARMY) return true; else return false; }); for(auto h : otherHeroes) { // Go to the other hero if we are faster if(!vstd::contains(ai->visitedHeroes[hero], h)) { vstd::concatenate(ret, ai->ah->howToVisitObj(hero, h)); } // Go to the other hero if we are faster if(!vstd::contains(ai->visitedHeroes[h], hero)) { vstd::concatenate(ret, ai->ah->howToVisitObj(h, hero.get())); } } std::vector objs; for(auto obj : ai->visitableObjs) { if(obj->ID == Obj::CREATURE_GENERATOR1) { auto relationToOwner = cb->getPlayerRelations(obj->getOwner(), ai->playerID); //Use flagged dwellings only when there are available creatures that we can afford if(relationToOwner == PlayerRelations::SAME_PLAYER) { auto dwelling = dynamic_cast(obj); ui32 val = std::min(value, howManyReinforcementsCanBuy(hero.get(), dwelling)); if(val) { for(auto & creLevel : dwelling->creatures) { if(creLevel.first) { for(auto & creatureID : creLevel.second) { auto creature = VLC->creh->creatures[creatureID]; if(ai->ah->freeResources().canAfford(creature->cost)) objs.push_back(obj); //TODO: reserve resources? } } } } } } } for(auto h : cb->getHeroesInfo()) { for(auto obj : objs) { //find safe dwelling if(ai->isGoodForVisit(obj, h)) { vstd::concatenate(ret, ai->ah->howToVisitObj(h, obj)); } } } if(ai->canRecruitAnyHero() && ai->ah->freeGold() > GameConstants::HERO_GOLD_COST) //this is not stupid in early phase of game { if(auto t = ai->findTownWithTavern()) { for(auto h : cb->getAvailableHeroes(t)) //we assume that all towns have same set of heroes { if(h && h->getTotalStrength() > 500) //do not buy heroes with single creatures for GatherArmy { ret.push_back(sptr(RecruitHero())); break; } } } } if(ret.empty()) { const bool allowGatherArmy = false; if(hero == ai->primaryHero()) ret.push_back(sptr(Explore(allowGatherArmy))); else throw cannotFulfillGoalException("No ways to gather army"); } return ret; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/GatherArmy.h000066400000000000000000000013511342332007200214460ustar00rootroot00000000000000/* * GatherArmy.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT GatherArmy : public CGoal { public: GatherArmy() : CGoal(Goals::GATHER_ARMY) { } GatherArmy(int val) : CGoal(Goals::GATHER_ARMY) { value = val; priority = 2.5; } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; virtual bool operator==(const GatherArmy & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/GatherTroops.cpp000066400000000000000000000072261342332007200223660ustar00rootroot00000000000000/* * GatherTroops.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool GatherTroops::operator==(const GatherTroops & other) const { return objid == other.objid; } int GatherTroops::getCreaturesCount(const CArmedInstance * army) { int count = 0; for(auto stack : army->Slots()) { if(objid == stack.second->getCreatureID().num) { count += stack.second->count; } } return count; } TSubgoal GatherTroops::whatToDoToAchieve() { logAi->trace("Entering GatherTroops::whatToDoToAchieve"); auto heroes = cb->getHeroesInfo(true); for(auto hero : heroes) { if(getCreaturesCount(hero) >= this->value) { logAi->trace("Completing GATHER_TROOPS by hero %s", hero->name); throw goalFulfilledException(sptr(*this)); } } TGoalVec solutions = getAllPossibleSubgoals(); if(solutions.empty()) return sptr(Explore()); return fh->chooseSolution(solutions); } TGoalVec GatherTroops::getAllPossibleSubgoals() { TGoalVec solutions; for(const CGTownInstance * t : cb->getTownsInfo()) { int count = getCreaturesCount(t->getUpperArmy()); if(count >= this->value) { if(t->visitingHero) { solutions.push_back(sptr(VisitObj(t->id.getNum()).sethero(t->visitingHero.get()))); } else { vstd::concatenate(solutions, ai->ah->howToVisitObj(t)); } continue; } auto creature = VLC->creh->creatures[objid]; if(t->subID == creature->faction) //TODO: how to force AI to build unupgraded creatures? :O { auto creatures = vstd::tryAt(t->town->creatures, creature->level - 1); if(!creatures) continue; int upgradeNumber = vstd::find_pos(*creatures, creature->idNumber); if(upgradeNumber < 0) continue; BuildingID bid(BuildingID::DWELL_FIRST + creature->level - 1 + upgradeNumber * GameConstants::CREATURES_PER_TOWN); if(t->hasBuilt(bid) && ai->ah->freeResources().canAfford(creature->cost)) //this assumes only creatures with dwellings are assigned to faction { solutions.push_back(sptr(BuyArmy(t, creature->AIValue * this->value).setobjid(objid))); } /*else //disable random building requests for now - this code needs to know a lot of town/resource context to do more good than harm { return sptr(BuildThis(bid, t).setpriority(priority)); }*/ } } for(auto obj : ai->visitableObjs) { auto d = dynamic_cast(obj); if(!d || obj->ID == Obj::TOWN) continue; for(auto creature : d->creatures) { if(creature.first) //there are more than 0 creatures avaliabe { for(auto type : creature.second) { if(type == objid && ai->ah->freeResources().canAfford(VLC->creh->creatures[type]->cost)) vstd::concatenate(solutions, ai->ah->howToVisitObj(obj)); } } } } return solutions; //TODO: exchange troops between heroes } bool GatherTroops::fulfillsMe(TSubgoal goal) { if (!hero || hero == goal->hero) //we got army for desired hero or any hero if (goal->objid == objid) //same creature type //TODO: consider upgrades? if (goal->value >= value) //notify every time we get resources? return true; return false; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/GatherTroops.h000066400000000000000000000015371342332007200220320ustar00rootroot00000000000000/* * GatherTroops.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT GatherTroops : public CGoal { public: GatherTroops() : CGoal(Goals::GATHER_TROOPS) { priority = 2; } GatherTroops(int type, int val) : CGoal(Goals::GATHER_TROOPS) { objid = type; value = val; priority = 2; } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; virtual bool operator==(const GatherTroops & other) const override; private: int getCreaturesCount(const CArmedInstance * army); }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/GetArtOfType.cpp000066400000000000000000000012671342332007200222610ustar00rootroot00000000000000/* * GetArtOfType.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "GetArtOfType.h" #include "FindObj.h" #include "../VCAI.h" #include "../AIUtility.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool GetArtOfType::operator==(const GetArtOfType & other) const { return other.hero.h == hero.h && other.objid == objid; } TSubgoal GetArtOfType::whatToDoToAchieve() { return sptr(FindObj(Obj::ARTIFACT, aid)); }vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/GetArtOfType.h000066400000000000000000000013421342332007200217200ustar00rootroot00000000000000/* * GetArtOfType.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT GetArtOfType : public CGoal { public: GetArtOfType() : CGoal(Goals::GET_ART_TYPE) { } GetArtOfType(int type) : CGoal(Goals::GET_ART_TYPE) { aid = type; priority = 2; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override; virtual bool operator==(const GetArtOfType & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Goals.h000066400000000000000000000013561342332007200204550ustar00rootroot00000000000000/* * Goals.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" #include "Invalid.h" #include "BuildBoat.h" #include "Build.h" #include "BuildThis.h" #include "Conquer.h" #include "GatherArmy.h" #include "Win.h" #include "VisitObj.h" #include "VisitTile.h" #include "VisitHero.h" #include "Explore.h" #include "BuyArmy.h" #include "GatherTroops.h" #include "Trade.h" #include "CollectRes.h" #include "RecruitHero.h" #include "GetArtOfType.h" #include "ClearWayTo.h" #include "DigAtTile.h" #include "FindObj.h" #include "CompleteQuest.h" #include "AdventureSpellCast.h"vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Invalid.h000066400000000000000000000012361342332007200207730ustar00rootroot00000000000000/* * Invalid.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; namespace Goals { class DLL_EXPORT Invalid : public CGoal { public: Invalid() : CGoal(Goals::INVALID) { priority = -1e10; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override { return iAmElementar(); } virtual bool operator==(const Invalid & other) const override { return true; } }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/RecruitHero.cpp000066400000000000000000000020151342332007200221670ustar00rootroot00000000000000/* * RecruitHero.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; TSubgoal RecruitHero::whatToDoToAchieve() { const CGTownInstance * t = ai->findTownWithTavern(); if(!t) return sptr(BuildThis(BuildingID::TAVERN).setpriority(2)); TResources res; res[Res::GOLD] = GameConstants::HERO_GOLD_COST; return ai->ah->whatToDo(res, iAmElementar()); //either buy immediately, or collect res } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/RecruitHero.h000066400000000000000000000012471342332007200216420ustar00rootroot00000000000000/* * RecruitHero.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT RecruitHero : public CGoal { public: RecruitHero() : CGoal(Goals::RECRUIT_HERO) { priority = 1; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override; virtual bool operator==(const RecruitHero & other) const override { return true; } }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Trade.cpp000066400000000000000000000006541342332007200210020ustar00rootroot00000000000000/* * Trade.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Trade.h" using namespace Goals; bool Trade::operator==(const Trade & other) const { return resID == other.resID; } TSubgoal Trade::whatToDoToAchieve() { return iAmElementar(); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Trade.h000066400000000000000000000013111342332007200204360ustar00rootroot00000000000000/* * Trade.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT Trade : public CGoal { public: Trade() : CGoal(Goals::TRADE) { } Trade(int rid, int val, int Objid) : CGoal(Goals::TRADE) { resID = rid; value = val; objid = Objid; priority = 3; //trading is instant, but picking resources is free } TSubgoal whatToDoToAchieve() override; virtual bool operator==(const Trade & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/VisitHero.cpp000066400000000000000000000037061342332007200216600ustar00rootroot00000000000000/* * VisitHero.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "VisitHero.h" #include "Explore.h" #include "Invalid.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool VisitHero::operator==(const VisitHero & other) const { return other.hero.h == hero.h && other.objid == objid; } std::string VisitHero::completeMessage() const { return "hero " + hero.get()->name + " visited hero " + boost::lexical_cast(objid); } TSubgoal VisitHero::whatToDoToAchieve() { const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(objid)); if(!obj) return sptr(Explore()); int3 pos = obj->visitablePos(); if(hero && ai->isAccessibleForHero(pos, hero, true) && isSafeToVisit(hero, pos)) //enemy heroes can get reinforcements { if(hero->pos == pos) logAi->error("Hero %s tries to visit himself.", hero.name); else { //can't use VISIT_TILE here as tile appears blocked by target hero //FIXME: elementar goal should not be abstract return sptr(VisitHero(objid).sethero(hero).settile(pos).setisElementar(true)); } } return sptr(Invalid()); } bool VisitHero::fulfillsMe(TSubgoal goal) { //TODO: VisitObj shoudl not be used for heroes, but... if(goal->goalType == VISIT_TILE) { auto obj = cb->getObj(ObjectInstanceID(objid)); if (!obj) { logAi->error("Hero %s: VisitHero::fulfillsMe at %s: object %d not found", hero.name, goal->tile.toString(), objid); return false; } return obj->visitablePos() == goal->tile; } return false; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/VisitHero.h000066400000000000000000000014471342332007200213250ustar00rootroot00000000000000/* * VisitHero.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT VisitHero : public CGoal { public: VisitHero() : CGoal(Goals::VISIT_HERO) { } VisitHero(int hid) : CGoal(Goals::VISIT_HERO) { objid = hid; priority = 4; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; std::string completeMessage() const override; virtual bool operator==(const VisitHero & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/VisitObj.cpp000066400000000000000000000053531342332007200214750ustar00rootroot00000000000000/* * VisitObj.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool VisitObj::operator==(const VisitObj & other) const { return other.hero.h == hero.h && other.objid == objid; } std::string VisitObj::completeMessage() const { return "hero " + hero.get()->name + " captured Object ID = " + boost::lexical_cast(objid); } TGoalVec VisitObj::getAllPossibleSubgoals() { TGoalVec goalList; const CGObjectInstance * obj = cb->getObjInstance(ObjectInstanceID(objid)); if(!obj) { throw cannotFulfillGoalException("Object is missing - goal is invalid now!"); } int3 pos = obj->visitablePos(); if(hero) { if(ai->isAccessibleForHero(pos, hero)) { if(isSafeToVisit(hero, pos)) goalList.push_back(sptr(VisitObj(obj->id.getNum()).sethero(hero))); else goalList.push_back(sptr(GatherArmy(evaluateDanger(pos, hero.h) * SAFE_ATTACK_CONSTANT).sethero(hero).setisAbstract(true))); return goalList; } } else { for(auto potentialVisitor : cb->getHeroesInfo()) { if(ai->isAccessibleForHero(pos, potentialVisitor)) { if(isSafeToVisit(potentialVisitor, pos)) goalList.push_back(sptr(VisitObj(obj->id.getNum()).sethero(potentialVisitor))); else goalList.push_back(sptr(GatherArmy(evaluateDanger(pos, potentialVisitor) * SAFE_ATTACK_CONSTANT).sethero(potentialVisitor).setisAbstract(true))); } } if(!goalList.empty()) { return goalList; } } goalList.push_back(sptr(ClearWayTo(pos))); return goalList; } TSubgoal VisitObj::whatToDoToAchieve() { auto bestGoal = fh->chooseSolution(getAllPossibleSubgoals()); if(bestGoal->goalType == VISIT_OBJ && bestGoal->hero) bestGoal->setisElementar(true); return bestGoal; } VisitObj::VisitObj(int Objid) : CGoal(VISIT_OBJ) { objid = Objid; tile = ai->myCb->getObjInstance(ObjectInstanceID(objid))->visitablePos(); priority = 3; } bool VisitObj::fulfillsMe(TSubgoal goal) { if(goal->goalType == VISIT_TILE) { if (!hero || hero == goal->hero) { auto obj = cb->getObjInstance(ObjectInstanceID(objid)); if (obj && obj->visitablePos() == goal->tile) //object could be removed return true; } } return false; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/VisitObj.h000066400000000000000000000013611342332007200211350ustar00rootroot00000000000000/* * VisitObj.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT VisitObj : public CGoal //this goal was previously known as GetObj { public: VisitObj() = delete; // empty constructor not allowed VisitObj(int Objid); TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; std::string completeMessage() const override; virtual bool operator==(const VisitObj & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/VisitTile.cpp000066400000000000000000000051731342332007200216600ustar00rootroot00000000000000/* * VisitTile.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; bool VisitTile::operator==(const VisitTile & other) const { return other.hero.h == hero.h && other.tile == tile; } std::string VisitTile::completeMessage() const { return "Hero " + hero.get()->name + " visited tile " + tile.toString(); } TSubgoal VisitTile::whatToDoToAchieve() { auto ret = fh->chooseSolution(getAllPossibleSubgoals()); if(ret->hero) { if(isSafeToVisit(ret->hero, tile) && ai->isAccessibleForHero(tile, ret->hero)) { ret->setisElementar(true); return ret; } else { return sptr(GatherArmy(evaluateDanger(tile, *ret->hero) * SAFE_ATTACK_CONSTANT) .sethero(ret->hero).setisAbstract(true)); } } return ret; } TGoalVec VisitTile::getAllPossibleSubgoals() { assert(cb->isInTheMap(tile)); TGoalVec ret; if(!cb->isVisible(tile)) ret.push_back(sptr(Explore())); //what sense does it make? else { std::vector heroes; if(hero) heroes.push_back(hero.h); //use assigned hero if any else heroes = cb->getHeroesInfo(); //use most convenient hero for(auto h : heroes) { if(ai->isAccessibleForHero(tile, h)) ret.push_back(sptr(VisitTile(tile).sethero(h))); } if(ai->canRecruitAnyHero()) ret.push_back(sptr(RecruitHero())); } if(ret.empty()) { auto obj = vstd::frontOrNull(cb->getVisitableObjs(tile)); if(obj && obj->ID == Obj::HERO && obj->tempOwner == ai->playerID) //our own hero stands on that tile { if(hero.get(true) && hero->id == obj->id) //if it's assigned hero, visit tile. If it's different hero, we can't visit tile now ret.push_back(sptr(VisitTile(tile).sethero(dynamic_cast(obj)).setisElementar(true))); else throw cannotFulfillGoalException("Tile is already occupied by another hero "); //FIXME: we should give up this tile earlier } else ret.push_back(sptr(ClearWayTo(tile))); } //important - at least one sub-goal must handle case which is impossible to fulfill (unreachable tile) return ret; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/VisitTile.h000066400000000000000000000014501342332007200213170ustar00rootroot00000000000000/* * VisitTile.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT VisitTile : public CGoal //tile, in conjunction with hero elementar; assumes tile is reachable { public: VisitTile() {} // empty constructor not allowed VisitTile(int3 Tile) : CGoal(Goals::VISIT_TILE) { tile = Tile; priority = 5; } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; virtual bool operator==(const VisitTile & other) const override; }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Win.cpp000066400000000000000000000131371342332007200205000ustar00rootroot00000000000000/* * Win.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "../VCAI.h" #include "../AIUtility.h" #include "../AIhelper.h" #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" #include "../../../lib/StringConstants.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; using namespace Goals; TSubgoal Win::whatToDoToAchieve() { auto toBool = [=](const EventCondition &) { // TODO: proper implementation // Right now even already fulfilled goals will be included into generated list // Proper check should test if event condition is already fulfilled // Easiest way to do this is to call CGameState::checkForVictory but this function should not be // used on client side or in AI code return false; }; std::vector goals; for(const TriggeredEvent & event : cb->getMapHeader()->triggeredEvents) { //TODO: try to eliminate human player(s) using loss conditions that have isHuman element if(event.effect.type == EventEffect::VICTORY) { boost::range::copy(event.trigger.getFulfillmentCandidates(toBool), std::back_inserter(goals)); } } //TODO: instead of returning first encountered goal AI should generate list of possible subgoals for(const EventCondition & goal : goals) { switch(goal.condition) { case EventCondition::HAVE_ARTIFACT: return sptr(GetArtOfType(goal.objectType)); case EventCondition::DESTROY: { if(goal.object) { auto obj = cb->getObj(goal.object->id); if(obj) if(obj->getOwner() == ai->playerID) //we can't capture our own object return sptr(Conquer()); return sptr(VisitObj(goal.object->id.getNum())); } else { // TODO: destroy all objects of type goal.objectType // This situation represents "kill all creatures" condition from H3 break; } } case EventCondition::HAVE_BUILDING: { // TODO build other buildings apart from Grail // goal.objectType = buidingID to build // goal.object = optional, town in which building should be built // Represents "Improve town" condition from H3 (but unlike H3 it consists from 2 separate conditions) if(goal.objectType == BuildingID::GRAIL) { if(auto h = ai->getHeroWithGrail()) { //hero is in a town that can host Grail if(h->visitedTown && !vstd::contains(h->visitedTown->forbiddenBuildings, BuildingID::GRAIL)) { const CGTownInstance * t = h->visitedTown; return sptr(BuildThis(BuildingID::GRAIL, t).setpriority(10)); } else { auto towns = cb->getTownsInfo(); towns.erase(boost::remove_if(towns, [](const CGTownInstance * t) -> bool { return vstd::contains(t->forbiddenBuildings, BuildingID::GRAIL); }), towns.end()); boost::sort(towns, CDistanceSorter(h.get())); if(towns.size()) { return sptr(VisitTile(towns.front()->visitablePos()).sethero(h)); } } } double ratio = 0; // maybe make this check a bit more complex? For example: // 0.75 -> dig randomly within 3 tiles radius // 0.85 -> radius now 2 tiles // 0.95 -> 1 tile radius, position is fully known // AFAIK H3 AI does something like this int3 grailPos = cb->getGrailPos(&ratio); if(ratio > 0.99) { return sptr(DigAtTile(grailPos)); } //TODO: use FIND_OBJ else if(const CGObjectInstance * obj = ai->getUnvisitedObj(objWithID)) //there are unvisited Obelisks return sptr(VisitObj(obj->id.getNum())); else return sptr(Explore()); } break; } case EventCondition::CONTROL: { if(goal.object) { auto objRelations = cb->getPlayerRelations(ai->playerID, goal.object->tempOwner); if(objRelations == PlayerRelations::ENEMIES) { return sptr(VisitObj(goal.object->id.getNum())); } else { // TODO: Defance break; } } else { //TODO: control all objects of type "goal.objectType" // Represents H3 condition "Flag all mines" break; } } case EventCondition::HAVE_RESOURCES: //TODO mines? piles? marketplace? //save? return sptr(CollectRes(static_cast(goal.objectType), goal.value)); case EventCondition::HAVE_CREATURES: return sptr(GatherTroops(goal.objectType, goal.value)); case EventCondition::TRANSPORT: { //TODO. merge with bring Grail to town? So AI will first dig grail, then transport it using this goal and builds it // Represents "transport artifact" condition: // goal.objectType = type of artifact // goal.object = destination-town where artifact should be transported break; } case EventCondition::STANDARD_WIN: return sptr(Conquer()); // Conditions that likely don't need any implementation case EventCondition::DAYS_PASSED: break; // goal.value = number of days for condition to trigger case EventCondition::DAYS_WITHOUT_TOWN: break; // goal.value = number of days to trigger this case EventCondition::IS_HUMAN: break; // Should be only used in calculation of candidates (see toBool lambda) case EventCondition::CONST_VALUE: break; case EventCondition::HAVE_0: case EventCondition::HAVE_BUILDING_0: case EventCondition::DESTROY_0: //TODO: support new condition format return sptr(Conquer()); default: assert(0); } } return sptr(Invalid()); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Goals/Win.h000066400000000000000000000011661342332007200201440ustar00rootroot00000000000000/* * Win.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "CGoal.h" struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { class DLL_EXPORT Win : public CGoal { public: Win() : CGoal(Goals::WIN) { priority = 100; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override; virtual bool operator==(const Win & other) const override { return true; } }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/MapObjectsEvaluator.cpp000066400000000000000000000110471342332007200226060ustar00rootroot00000000000000#include "StdInc.h" #include "MapObjectsEvaluator.h" #include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/CRandomGenerator.h" #include "../../lib/spells/CSpellHandler.h" MapObjectsEvaluator & MapObjectsEvaluator::getInstance() { static std::unique_ptr singletonInstance; if(singletonInstance == nullptr) singletonInstance.reset(new MapObjectsEvaluator()); return *(singletonInstance.get()); } MapObjectsEvaluator::MapObjectsEvaluator() { for(auto primaryID : VLC->objtypeh->knownObjects()) { for(auto secondaryID : VLC->objtypeh->knownSubObjects(primaryID)) { auto handler = VLC->objtypeh->getHandlerFor(primaryID, secondaryID); if(!handler->isStaticObject()) { if(handler->getAiValue() != boost::none) { objectDatabase[CompoundMapObjectID(primaryID, secondaryID)] = handler->getAiValue().get(); } else if(VLC->objtypeh->getObjGroupAiValue(primaryID) != boost::none) //if value is not initialized - fallback to default value for this object family if it exists { objectDatabase[CompoundMapObjectID(primaryID, secondaryID)] = VLC->objtypeh->getObjGroupAiValue(primaryID).get(); } else //some default handling when aiValue not found, objects that require advanced properties (unavailable from handler) get their value calculated in getObjectValue { objectDatabase[CompoundMapObjectID(primaryID, secondaryID)] = 0; } } } } } boost::optional MapObjectsEvaluator::getObjectValue(int primaryID, int secondaryID) const { CompoundMapObjectID internalIdentifier = CompoundMapObjectID(primaryID, secondaryID); auto object = objectDatabase.find(internalIdentifier); if(object != objectDatabase.end()) return object->second; logGlobal->trace("Unknown object for AI, ID: " + std::to_string(primaryID) + ", SubID: " + std::to_string(secondaryID)); return boost::optional(); } boost::optional MapObjectsEvaluator::getObjectValue(const CGObjectInstance * obj) const { if(obj->ID == Obj::HERO) { //special case handling: in-game heroes have hero ID as object subID, but when reading configs available hero object subID's are hero classes auto hero = dynamic_cast(obj); return getObjectValue(obj->ID, hero->type->heroClass->id); } if(obj->ID == Obj::CREATURE_GENERATOR1 || obj->ID == Obj::CREATURE_GENERATOR4) { auto dwelling = dynamic_cast(obj); int aiValue = 0; for(auto & creLevel : dwelling->creatures) { for(auto & creatureID : creLevel.second) { auto creature = VLC->creh->creatures[creatureID]; aiValue += (creature->AIValue * creature->growth); } } return aiValue; } else if(obj->ID == Obj::ARTIFACT) { auto artifactObject = dynamic_cast(obj); switch(artifactObject->storedArtifact->artType->aClass) { case CArtifact::EartClass::ART_TREASURE: return 2000; case CArtifact::EartClass::ART_MINOR: return 5000; case CArtifact::EartClass::ART_MAJOR: return 10000; case CArtifact::EartClass::ART_RELIC: return 20000; case CArtifact::EartClass::ART_SPECIAL: return 20000; default: return 0; //invalid artifact class } } else if(obj->ID == Obj::SPELL_SCROLL) { auto scrollObject = dynamic_cast(obj); auto spell = scrollObject->storedArtifact->getGivenSpellID().toSpell(); if(spell) { switch(spell->getLevel()) { case 0: return 0; //scroll with creature ability? Let's assume it is useless case 1: return 1000; case 2: return 2000; case 3: return 5000; case 4: return 10000; case 5: return 20000; default: logAi->warn("AI detected spell scroll with spell level %s", spell->getLevel()); } } else logAi->warn("AI found spell scroll with invalid spell ID: %s", scrollObject->storedArtifact->getGivenSpellID()); } return getObjectValue(obj->ID, obj->subID); } void MapObjectsEvaluator::addObjectData(int primaryID, int secondaryID, int value) //by current design it updates value if already in AI database { CompoundMapObjectID internalIdentifier = CompoundMapObjectID(primaryID, secondaryID); objectDatabase[internalIdentifier] = value; } void MapObjectsEvaluator::removeObjectData(int primaryID, int secondaryID) { CompoundMapObjectID internalIdentifier = CompoundMapObjectID(primaryID, secondaryID); vstd::erase_if_present(objectDatabase, internalIdentifier); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/MapObjectsEvaluator.h000066400000000000000000000013771342332007200222600ustar00rootroot00000000000000/* * MapObjectsEvaluator.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/mapObjects/CObjectClassesHandler.h" class MapObjectsEvaluator { private: std::map objectDatabase; //value for each object type public: MapObjectsEvaluator(); static MapObjectsEvaluator & getInstance(); boost::optional getObjectValue(int primaryID, int secondaryID) const; boost::optional getObjectValue(const CGObjectInstance * obj) const; void addObjectData(int primaryID, int secondaryID, int value); void removeObjectData(int primaryID, int secondaryID); }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Pathfinding/000077500000000000000000000000001342332007200204205ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Pathfinding/AINodeStorage.cpp000066400000000000000000000255211342332007200235550ustar00rootroot00000000000000/* * AINodeStorage.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "AINodeStorage.h" #include "../Goals/Goals.h" #include "../../../CCallback.h" #include "../../../lib/mapping/CMap.h" #include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/PathfinderUtil.h" #include "../../../lib/CPlayerState.h" extern boost::thread_specific_ptr cb; AINodeStorage::AINodeStorage(const int3 & Sizes) : sizes(Sizes) { nodes.resize(boost::extents[sizes.x][sizes.y][sizes.z][EPathfindingLayer::NUM_LAYERS][NUM_CHAINS]); } AINodeStorage::~AINodeStorage() = default; void AINodeStorage::initialize(const PathfinderOptions & options, const CGameState * gs, const CGHeroInstance * hero) { //TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline int3 pos; const int3 sizes = gs->getMapSize(); const auto & fow = static_cast(gs)->getPlayerTeam(hero->tempOwner)->fogOfWarMap; const PlayerColor player = hero->tempOwner; //make 200% sure that these are loop invariants (also a bit shorter code), let compiler do the rest(loop unswitching) const bool useFlying = options.useFlying; const bool useWaterWalking = options.useWaterWalking; for(pos.x=0; pos.x < sizes.x; ++pos.x) { for(pos.y=0; pos.y < sizes.y; ++pos.y) { for(pos.z=0; pos.z < sizes.z; ++pos.z) { const TerrainTile * tile = &gs->map->getTile(pos); switch(tile->terType) { case ETerrainType::ROCK: break; case ETerrainType::WATER: resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); if(useFlying) resetTile(pos, ELayer::AIR, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); if(useWaterWalking) resetTile(pos, ELayer::WATER, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); break; default: resetTile(pos, ELayer::LAND, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); if(useFlying) resetTile(pos, ELayer::AIR, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); break; } } } } } const AIPathNode * AINodeStorage::getAINode(const CGPathNode * node) const { return static_cast(node); } void AINodeStorage::updateAINode(CGPathNode * node, std::function updater) { auto aiNode = static_cast(node); updater(aiNode); } bool AINodeStorage::isBattleNode(const CGPathNode * node) const { return (getAINode(node)->chainMask & BATTLE_CHAIN) > 0; } boost::optional AINodeStorage::getOrCreateNode(const int3 & pos, const EPathfindingLayer layer, int chainNumber) { auto chains = nodes[pos.x][pos.y][pos.z][layer]; for(AIPathNode & node : chains) { if(node.chainMask == chainNumber) { return &node; } if(node.chainMask == 0) { node.chainMask = chainNumber; return &node; } } return boost::none; } CGPathNode * AINodeStorage::getInitialNode() { auto hpos = hero->getPosition(false); auto initialNode = getOrCreateNode(hpos, hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND, NORMAL_CHAIN) .get(); initialNode->turns = 0; initialNode->moveRemains = hero->movement; initialNode->danger = 0; initialNode->cost = 0.0; return initialNode; } void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility) { for(int i = 0; i < NUM_CHAINS; i++) { AIPathNode & heroNode = nodes[coord.x][coord.y][coord.z][layer][i]; heroNode.chainMask = 0; heroNode.danger = 0; heroNode.manaCost = 0; heroNode.specialAction.reset(); heroNode.update(coord, layer, accessibility); } } void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) { const AIPathNode * srcNode = getAINode(source.node); updateAINode(destination.node, [&](AIPathNode * dstNode) { dstNode->moveRemains = destination.movementLeft; dstNode->turns = destination.turn; dstNode->cost = destination.cost; dstNode->danger = srcNode->danger; dstNode->action = destination.action; dstNode->theNodeBefore = srcNode->theNodeBefore; dstNode->manaCost = srcNode->manaCost; if(dstNode->specialAction) { dstNode->specialAction->applyOnDestination(getHero(), destination, source, dstNode, srcNode); } }); } std::vector AINodeStorage::calculateNeighbours( const PathNodeInfo & source, const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) { std::vector neighbours; neighbours.reserve(16); const AIPathNode * srcNode = getAINode(source.node); auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source); for(auto & neighbour : accessibleNeighbourTiles) { for(EPathfindingLayer i = EPathfindingLayer::LAND; i <= EPathfindingLayer::AIR; i.advance(1)) { auto nextNode = getOrCreateNode(neighbour, i, srcNode->chainMask); if(!nextNode || nextNode.get()->accessible == CGPathNode::NOT_SET) continue; neighbours.push_back(nextNode.get()); } } return neighbours; } void AINodeStorage::setHero(HeroPtr heroPtr) { hero = heroPtr.get(); } class TownPortalAction : public ISpecialAction { private: const CGTownInstance * target; const HeroPtr hero; public: TownPortalAction(const CGTownInstance * target) :target(target) { } virtual Goals::TSubgoal whatToDo(HeroPtr hero) const override { const CGTownInstance * targetTown = target; // const pointer is not allowed in settown return sptr(Goals::AdventureSpellCast(hero, SpellID::TOWN_PORTAL).settown(targetTown).settile(targetTown->visitablePos())); } }; std::vector AINodeStorage::calculateTeleportations( const PathNodeInfo & source, const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) { std::vector neighbours; if(source.isNodeObjectVisitable()) { auto accessibleExits = pathfinderHelper->getTeleportExits(source); auto srcNode = getAINode(source.node); for(auto & neighbour : accessibleExits) { auto node = getOrCreateNode(neighbour, source.node->layer, srcNode->chainMask); if(!node) continue; neighbours.push_back(node.get()); } } if(hero->getPosition(false) == source.coord) { calculateTownPortalTeleportations(source, neighbours); } return neighbours; } void AINodeStorage::calculateTownPortalTeleportations( const PathNodeInfo & source, std::vector & neighbours) { SpellID spellID = SpellID::TOWN_PORTAL; const CSpell * townPortal = spellID.toSpell(); auto srcNode = getAINode(source.node); if(hero->canCastThisSpell(townPortal) && hero->mana >= hero->getSpellCost(townPortal)) { auto towns = cb->getTownsInfo(false); vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool { return cb->getPlayerRelations(hero->tempOwner, t->tempOwner) == PlayerRelations::ENEMIES; }); if(!towns.size()) { return; } // TODO: Copy/Paste from TownPortalMechanics auto skillLevel = hero->getSpellSchoolLevel(townPortal); auto movementCost = GameConstants::BASE_MOVEMENT_COST * (skillLevel >= 3 ? 2 : 3); if(hero->movement < movementCost) { return; } if(skillLevel < SecSkillLevel::ADVANCED) { const CGTownInstance * nearestTown = *vstd::minElementByFun(towns, [&](const CGTownInstance * t) -> int { return hero->visitablePos().dist2dSQ(t->visitablePos()); }); towns = std::vector{ nearestTown }; } for(const CGTownInstance * targetTown : towns) { if(targetTown->visitingHero) continue; auto nodeOptional = getOrCreateNode(targetTown->visitablePos(), EPathfindingLayer::LAND, srcNode->chainMask | CAST_CHAIN); if(nodeOptional) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace("Adding town portal node at %s", targetTown->name); #endif AIPathNode * node = nodeOptional.get(); node->theNodeBefore = source.node; node->specialAction.reset(new TownPortalAction(targetTown)); node->moveRemains = source.node->moveRemains; neighbours.push_back(node); } } } } bool AINodeStorage::hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const { auto pos = destination.coord; auto chains = nodes[pos.x][pos.y][pos.z][EPathfindingLayer::LAND]; auto destinationNode = getAINode(destination.node); for(const AIPathNode & node : chains) { auto sameNode = node.chainMask == destinationNode->chainMask; if(sameNode || node.action == CGPathNode::ENodeAction::UNKNOWN) { continue; } if(node.danger <= destinationNode->danger && destinationNode->chainMask == 1 && node.chainMask == 0) { if(node.cost < destinationNode->cost) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Block ineficient move %s:->%s, mask=%i, mp diff: %i", source.coord.toString(), destination.coord.toString(), destinationNode->chainMask, node.moveRemains - destinationNode->moveRemains); #endif return true; } } } return false; } bool AINodeStorage::isTileAccessible(const int3 & pos, const EPathfindingLayer layer) const { const AIPathNode & node = nodes[pos.x][pos.y][pos.z][layer][0]; return node.action != CGPathNode::ENodeAction::UNKNOWN; } std::vector AINodeStorage::getChainInfo(int3 pos, bool isOnLand) const { std::vector paths; auto chains = nodes[pos.x][pos.y][pos.z][isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL]; auto initialPos = hero->visitablePos(); for(const AIPathNode & node : chains) { if(node.action == CGPathNode::ENodeAction::UNKNOWN) { continue; } AIPath path; const AIPathNode * current = &node; while(current != nullptr && current->coord != initialPos) { AIPathNodeInfo pathNode; pathNode.cost = current->cost; pathNode.turns = current->turns; pathNode.danger = current->danger; pathNode.coord = current->coord; path.nodes.push_back(pathNode); path.specialAction = current->specialAction; current = getAINode(current->theNodeBefore); } paths.push_back(path); } return paths; } AIPath::AIPath() : nodes({}) { } int3 AIPath::firstTileToGet() const { if(nodes.size()) { return nodes.back().coord; } return int3(-1, -1, -1); } uint64_t AIPath::getPathDanger() const { if(nodes.size()) { return nodes.front().danger; } return 0; } float AIPath::movementCost() const { if(nodes.size()) { return nodes.front().cost; } // TODO: boost:optional? return 0.0; } uint64_t AIPath::getTotalDanger(HeroPtr hero) const { uint64_t pathDanger = getPathDanger(); uint64_t objDanger = evaluateDanger(nodes.front().coord, hero.get()); // bank danger is not checked by pathfinder uint64_t danger = pathDanger > objDanger ? pathDanger : objDanger; return danger; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Pathfinding/AINodeStorage.h000066400000000000000000000067341342332007200232270ustar00rootroot00000000000000/* * AINodeStorage.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../../lib/CPathfinder.h" #include "../../../lib/mapObjects/CGHeroInstance.h" #include "../AIUtility.h" #include "../Goals/AbstractGoal.h" struct AIPathNode; class ISpecialAction { public: virtual Goals::TSubgoal whatToDo(HeroPtr hero) const = 0; virtual void applyOnDestination( HeroPtr hero, CDestinationNodeInfo & destination, const PathNodeInfo & source, AIPathNode * dstMode, const AIPathNode * srcNode) const { } }; struct AIPathNode : public CGPathNode { uint32_t chainMask; uint64_t danger; uint32_t manaCost; std::shared_ptr specialAction; }; struct AIPathNodeInfo { float cost; int turns; int3 coord; uint64_t danger; }; struct AIPath { std::vector nodes; std::shared_ptr specialAction; AIPath(); /// Gets danger of path excluding danger of visiting the target object like creature bank uint64_t getPathDanger() const; /// Gets danger of path including danger of visiting the target object like creature bank uint64_t getTotalDanger(HeroPtr hero) const; int3 firstTileToGet() const; float movementCost() const; }; class AINodeStorage : public INodeStorage { private: int3 sizes; /// 1-3 - position on map, 4 - layer (air, water, land), 5 - chain (normal, battle, spellcast and combinations) boost::multi_array nodes; const CGHeroInstance * hero; STRONG_INLINE void resetTile(const int3 & tile, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility); public: /// more than 1 chain layer allows us to have more than 1 path to each tile so we can chose more optimal one. static const int NUM_CHAINS = 3; // chain flags, can be combined static const int NORMAL_CHAIN = 1; static const int BATTLE_CHAIN = 2; static const int CAST_CHAIN = 4; static const int RESOURCE_CHAIN = 8; AINodeStorage(const int3 & sizes); ~AINodeStorage(); void initialize(const PathfinderOptions & options, const CGameState * gs, const CGHeroInstance * hero) override; virtual CGPathNode * getInitialNode() override; virtual std::vector calculateNeighbours( const PathNodeInfo & source, const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) override; virtual std::vector calculateTeleportations( const PathNodeInfo & source, const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) override; virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; const AIPathNode * getAINode(const CGPathNode * node) const; void updateAINode(CGPathNode * node, std::function updater); bool isBattleNode(const CGPathNode * node) const; bool hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const; boost::optional getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, int chainNumber); std::vector getChainInfo(int3 pos, bool isOnLand) const; bool isTileAccessible(const int3 & pos, const EPathfindingLayer layer) const; void setHero(HeroPtr heroPtr); const CGHeroInstance * getHero() const { return hero; } private: void calculateTownPortalTeleportations(const PathNodeInfo & source, std::vector & neighbours); }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Pathfinding/AIPathfinder.cpp000066400000000000000000000045221342332007200234250ustar00rootroot00000000000000/* * AIPathfinder.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "AIPathfinder.h" #include "AIPathfinderConfig.h" #include "../../../CCallback.h" #include "../../../lib/mapping/CMap.h" std::vector> AIPathfinder::storagePool; std::map> AIPathfinder::storageMap; boost::mutex AIPathfinder::storageMutex; AIPathfinder::AIPathfinder(CPlayerSpecificInfoCallback * cb, VCAI * ai) :cb(cb), ai(ai) { } void AIPathfinder::clear() { boost::unique_lock storageLock(storageMutex); storageMap.clear(); } void AIPathfinder::init() { boost::unique_lock storageLock(storageMutex); storagePool.clear(); storageMap.clear(); } bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) { boost::unique_lock storageLock(storageMutex); std::shared_ptr nodeStorage = getOrCreateStorage(hero); return nodeStorage->isTileAccessible(tile, EPathfindingLayer::LAND) || nodeStorage->isTileAccessible(tile, EPathfindingLayer::SAIL); } std::vector AIPathfinder::getPathInfo(HeroPtr hero, int3 tile) { boost::unique_lock storageLock(storageMutex); std::shared_ptr nodeStorage = getOrCreateStorage(hero); const TerrainTile * tileInfo = cb->getTile(tile, false); if(!tileInfo) { return std::vector(); } return nodeStorage->getChainInfo(tile, !tileInfo->isWater()); } std::shared_ptr AIPathfinder::getOrCreateStorage(const HeroPtr & hero) { std::shared_ptr nodeStorage; if(!vstd::contains(storageMap, hero)) { logAi->debug("Recalculate paths for %s", hero->name); if(storageMap.size() < storagePool.size()) { nodeStorage = storagePool.at(storageMap.size()); } else { nodeStorage = std::make_shared(cb->getMapSize()); storagePool.push_back(nodeStorage); } storageMap[hero] = nodeStorage; nodeStorage->setHero(hero.get()); auto config = std::make_shared(cb, ai, nodeStorage); cb->calculatePaths(config, hero.get()); } else { nodeStorage = storageMap.at(hero); } return nodeStorage; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Pathfinding/AIPathfinder.h000066400000000000000000000015301342332007200230660ustar00rootroot00000000000000/* * AIPathfinder.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "AINodeStorage.h" #include "../AIUtility.h" #include "../VCAI.h" class AIPathfinder { private: static std::vector> storagePool; static std::map> storageMap; static boost::mutex storageMutex; CPlayerSpecificInfoCallback * cb; VCAI * ai; std::shared_ptr getOrCreateStorage(const HeroPtr & hero); public: AIPathfinder(CPlayerSpecificInfoCallback * cb, VCAI * ai); std::vector getPathInfo(HeroPtr hero, int3 tile); bool isTileAccessible(const HeroPtr & hero, const int3 & tile); void clear(); void init(); }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp000066400000000000000000000317421342332007200245570ustar00rootroot00000000000000/* * AIPathfinderConfig.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "AIPathfinderConfig.h" #include "../Goals/Goals.h" #include "../../../CCallback.h" #include "../../../lib/mapping/CMap.h" #include "../../../lib/mapObjects/MapObjects.h" namespace AIPathfinding { class VirtualBoatAction : public ISpecialAction { private: uint64_t specialChain; public: VirtualBoatAction(uint64_t specialChain) :specialChain(specialChain) { } uint64_t getSpecialChain() const { return specialChain; } }; class BuildBoatAction : public VirtualBoatAction { private: const IShipyard * shipyard; public: BuildBoatAction(const IShipyard * shipyard) :VirtualBoatAction(AINodeStorage::RESOURCE_CHAIN), shipyard(shipyard) { } virtual Goals::TSubgoal whatToDo(HeroPtr hero) const override { return sptr(Goals::BuildBoat(shipyard)); } }; class SummonBoatAction : public VirtualBoatAction { public: SummonBoatAction() :VirtualBoatAction(AINodeStorage::CAST_CHAIN) { } virtual Goals::TSubgoal whatToDo(HeroPtr hero) const override { return sptr(Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT)); } virtual void applyOnDestination( HeroPtr hero, CDestinationNodeInfo & destination, const PathNodeInfo & source, AIPathNode * dstMode, const AIPathNode * srcNode) const override { dstMode->manaCost = srcNode->manaCost + getManaCost(hero); dstMode->theNodeBefore = source.node; } bool isAffordableBy(HeroPtr hero, const AIPathNode * source) const { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Hero %s has %d mana and needed %d and already spent %d", hero->name, hero->mana, getManaCost(hero), source->manaCost); #endif return hero->mana >= source->manaCost + getManaCost(hero); } private: uint32_t getManaCost(HeroPtr hero) const { SpellID summonBoat = SpellID::SUMMON_BOAT; return hero->getSpellCost(summonBoat.toSpell()); } }; class BattleAction : public ISpecialAction { private: const int3 target; const HeroPtr hero; public: BattleAction(const int3 target) :target(target) { } virtual Goals::TSubgoal whatToDo(HeroPtr hero) const override { return sptr(Goals::VisitTile(target).sethero(hero)); } }; class AILayerTransitionRule : public LayerTransitionRule { private: CPlayerSpecificInfoCallback * cb; VCAI * ai; std::map> virtualBoats; std::shared_ptr nodeStorage; std::shared_ptr summonableVirtualBoat; public: AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage) :cb(cb), ai(ai), nodeStorage(nodeStorage) { setup(); } virtual void process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const override { LayerTransitionRule::process(source, destination, pathfinderConfig, pathfinderHelper); if(!destination.blocked) { return; } if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL) { std::shared_ptr virtualBoat = findVirtualBoat(destination, source); if(virtualBoat && tryEmbarkVirtualBoat(destination, source, virtualBoat)) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); #endif } } } private: void setup() { std::vector shipyards; for(const CGTownInstance * t : cb->getTownsInfo()) { if(t->hasBuilt(BuildingID::SHIPYARD)) shipyards.push_back(t); } for(const CGObjectInstance * obj : ai->visitableObjs) { if(obj->ID != Obj::TOWN) //towns were handled in the previous loop { if(const IShipyard * shipyard = IShipyard::castFrom(obj)) shipyards.push_back(shipyard); } } for(const IShipyard * shipyard : shipyards) { if(shipyard->shipyardStatus() == IShipyard::GOOD) { int3 boatLocation = shipyard->bestLocation(); virtualBoats[boatLocation] = std::make_shared(shipyard); logAi->debug("Virtual boat added at %s", boatLocation.toString()); } } auto hero = nodeStorage->getHero(); auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); if(hero->canCastThisSpell(summonBoatSpell) && hero->getSpellSchoolLevel(summonBoatSpell) >= SecSkillLevel::ADVANCED) { // TODO: For lower school level we might need to check the existance of some boat summonableVirtualBoat.reset(new SummonBoatAction()); } } std::shared_ptr findVirtualBoat( CDestinationNodeInfo &destination, const PathNodeInfo &source) const { std::shared_ptr virtualBoat; if(vstd::contains(virtualBoats, destination.coord)) { virtualBoat = virtualBoats.at(destination.coord); } else if( summonableVirtualBoat && summonableVirtualBoat->isAffordableBy(nodeStorage->getHero(), nodeStorage->getAINode(source.node))) { virtualBoat = summonableVirtualBoat; } return virtualBoat; } bool tryEmbarkVirtualBoat( CDestinationNodeInfo &destination, const PathNodeInfo &source, std::shared_ptr virtualBoat) const { bool result = false; nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) { auto boatNodeOptional = nodeStorage->getOrCreateNode( node->coord, node->layer, node->chainMask | virtualBoat->getSpecialChain()); if(boatNodeOptional) { AIPathNode * boatNode = boatNodeOptional.get(); if(boatNode->action == CGPathNode::UNKNOWN) { boatNode->specialAction = virtualBoat; destination.blocked = false; destination.action = CGPathNode::ENodeAction::EMBARK; destination.node = boatNode; result = true; } else { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Special transition node already allocated. Blocked moving %s -> %s", source.coord.toString(), destination.coord.toString()); #endif } } else { logAi->debug( "Can not allocate special transition node while moving %s -> %s", source.coord.toString(), destination.coord.toString()); } }); return result; } }; class AIMovementAfterDestinationRule : public MovementAfterDestinationRule { private: CPlayerSpecificInfoCallback * cb; std::shared_ptr nodeStorage; public: AIMovementAfterDestinationRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr nodeStorage) :cb(cb), nodeStorage(nodeStorage) { } virtual void process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const override { if(nodeStorage->hasBetterChain(source, destination)) { destination.blocked = true; return; } auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper); if(blocker == BlockingReason::NONE) return; if(blocker == BlockingReason::DESTINATION_BLOCKVIS && destination.nodeObject) { auto objID = destination.nodeObject->ID; auto enemyHero = objID == Obj::HERO && destination.objectRelations == PlayerRelations::ENEMIES; if(!enemyHero && !isObjectRemovable(destination.nodeObject)) { destination.blocked = true; } return; } if(blocker == BlockingReason::DESTINATION_VISIT) { return; } if(blocker == BlockingReason::DESTINATION_GUARDED) { auto srcGuardians = cb->getGuardingCreatures(source.coord); auto destGuardians = cb->getGuardingCreatures(destination.coord); if(destGuardians.empty()) { destination.blocked = true; return; } vstd::erase_if(destGuardians, [&](const CGObjectInstance * destGuard) -> bool { return vstd::contains(srcGuardians, destGuard); }); auto guardsAlreadyBypassed = destGuardians.empty() && srcGuardians.size(); if(guardsAlreadyBypassed && nodeStorage->isBattleNode(source.node)) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Bypass guard at destination while moving %s -> %s", source.coord.toString(), destination.coord.toString()); #endif return; } const AIPathNode * destNode = nodeStorage->getAINode(destination.node); auto battleNodeOptional = nodeStorage->getOrCreateNode( destination.coord, destination.node->layer, destNode->chainMask | AINodeStorage::BATTLE_CHAIN); if(!battleNodeOptional) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Can not allocate battle node while moving %s -> %s", source.coord.toString(), destination.coord.toString()); #endif destination.blocked = true; return; } AIPathNode * battleNode = battleNodeOptional.get(); if(battleNode->locked) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Block bypass guard at destination while moving %s -> %s", source.coord.toString(), destination.coord.toString()); #endif destination.blocked = true; return; } auto hero = nodeStorage->getHero(); auto danger = evaluateDanger(destination.coord, hero); destination.node = battleNode; nodeStorage->commit(destination, source); if(battleNode->danger < danger) { battleNode->danger = danger; } battleNode->specialAction = std::make_shared(destination.coord); #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Begin bypass guard at destination with danger %s while moving %s -> %s", std::to_string(danger), source.coord.toString(), destination.coord.toString()); #endif return; } destination.blocked = true; } }; class AIMovementToDestinationRule : public MovementToDestinationRule { private: std::shared_ptr nodeStorage; public: AIMovementToDestinationRule(std::shared_ptr nodeStorage) : nodeStorage(nodeStorage) { } virtual void process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const override { auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper); if(blocker == BlockingReason::NONE) return; if(blocker == BlockingReason::DESTINATION_BLOCKED && destination.action == CGPathNode::EMBARK && nodeStorage->getAINode(destination.node)->specialAction) { return; } if(blocker == BlockingReason::SOURCE_GUARDED && nodeStorage->isBattleNode(source.node)) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Bypass src guard while moving from %s to %s", source.coord.toString(), destination.coord.toString()); #endif return; } destination.blocked = true; } }; class AIPreviousNodeRule : public MovementToDestinationRule { private: std::shared_ptr nodeStorage; public: AIPreviousNodeRule(std::shared_ptr nodeStorage) : nodeStorage(nodeStorage) { } virtual void process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const override { if(source.node->action == CGPathNode::ENodeAction::BLOCKING_VISIT || source.node->action == CGPathNode::ENodeAction::VISIT) { // we can not directly bypass objects, we need to interact with them first destination.node->theNodeBefore = source.node; #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Link src node %s to destination node %s while bypassing visitable obj", source.coord.toString(), destination.coord.toString()); #endif return; } auto aiSourceNode = nodeStorage->getAINode(source.node); if(aiSourceNode->specialAction) { // there is some action on source tile which should be performed before we can bypass it destination.node->theNodeBefore = source.node; } } }; std::vector> makeRuleset( CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage) { std::vector> rules = { std::make_shared(cb, ai, nodeStorage), std::make_shared(), std::make_shared(nodeStorage), std::make_shared(), std::make_shared(nodeStorage), std::make_shared(cb, nodeStorage) }; return rules; } AIPathfinderConfig::AIPathfinderConfig( CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage) :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)) { } } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Pathfinding/AIPathfinderConfig.h000066400000000000000000000007731342332007200242240ustar00rootroot00000000000000/* * AIPathfinderConfig.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "AINodeStorage.h" #include "../VCAI.h" namespace AIPathfinding { class AIPathfinderConfig : public PathfinderConfig { public: AIPathfinderConfig( CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage); }; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Pathfinding/PathfindingManager.cpp000066400000000000000000000141571342332007200246620ustar00rootroot00000000000000/* * PathfindingManager.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "PathfindingManager.h" #include "AIPathfinder.h" #include "AIPathfinderConfig.h" #include "../Goals/Goals.h" #include "../../../lib/CGameInfoCallback.h" #include "../../../lib/mapping/CMap.h" PathfindingManager::PathfindingManager(CPlayerSpecificInfoCallback * CB, VCAI * AI) : ai(AI), cb(CB) { } void PathfindingManager::init(CPlayerSpecificInfoCallback * CB) { cb = CB; pathfinder.reset(new AIPathfinder(cb, ai)); pathfinder->init(); } void PathfindingManager::setAI(VCAI * AI) { ai = AI; } Goals::TGoalVec PathfindingManager::howToVisitTile(int3 tile) { Goals::TGoalVec result; auto heroes = cb->getHeroesInfo(); result.reserve(heroes.size()); for(auto hero : heroes) { vstd::concatenate(result, howToVisitTile(hero, tile)); } return result; } Goals::TGoalVec PathfindingManager::howToVisitObj(ObjectIdRef obj) { Goals::TGoalVec result; auto heroes = cb->getHeroesInfo(); result.reserve(heroes.size()); for(auto hero : heroes) { vstd::concatenate(result, howToVisitObj(hero, obj)); } return result; } Goals::TGoalVec PathfindingManager::howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy) { auto result = findPath(hero, tile, allowGatherArmy, [&](int3 firstTileToGet) -> Goals::TSubgoal { return sptr(Goals::VisitTile(firstTileToGet).sethero(hero).setisAbstract(true)); }); for(Goals::TSubgoal solution : result) { solution->setparent(sptr(Goals::VisitTile(tile).sethero(hero).setevaluationContext(solution->evaluationContext))); } return result; } Goals::TGoalVec PathfindingManager::howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy) { if(!obj) { return Goals::TGoalVec(); } int3 dest = obj->visitablePos(); auto result = findPath(hero, dest, allowGatherArmy, [&](int3 firstTileToGet) -> Goals::TSubgoal { if(obj->ID.num == Obj::HERO && obj->getOwner() == hero->getOwner()) return sptr(Goals::VisitHero(obj->id.getNum()).sethero(hero).setisAbstract(true)); else return sptr(Goals::VisitObj(obj->id.getNum()).sethero(hero).setisAbstract(true)); }); for(Goals::TSubgoal solution : result) { solution->setparent(sptr(Goals::VisitObj(obj->id.getNum()).sethero(hero).setevaluationContext(solution->evaluationContext))); } return result; } std::vector PathfindingManager::getPathsToTile(HeroPtr hero, int3 tile) { return pathfinder->getPathInfo(hero, tile); } Goals::TGoalVec PathfindingManager::findPath( HeroPtr hero, crint3 dest, bool allowGatherArmy, const std::function doVisitTile) { Goals::TGoalVec result; boost::optional armyValueRequired; uint64_t danger; std::vector chainInfo = pathfinder->getPathInfo(hero, dest); #ifdef VCMI_TRACE_PATHFINDER logAi->trace("Trying to find a way for %s to visit tile %s", hero->name, dest.toString()); #endif for(auto path : chainInfo) { int3 firstTileToGet = path.firstTileToGet(); #ifdef VCMI_TRACE_PATHFINDER logAi->trace("Path found size=%i, first tile=%s", path.nodes.size(), firstTileToGet.toString()); #endif if(firstTileToGet.valid() && ai->isTileNotReserved(hero.get(), firstTileToGet)) { danger = path.getTotalDanger(hero); if(isSafeToVisit(hero, danger)) { Goals::TSubgoal solution; if(path.specialAction) { solution = path.specialAction->whatToDo(hero); } else { solution = dest == firstTileToGet ? doVisitTile(firstTileToGet) : clearWayTo(hero, firstTileToGet); } if(solution->invalid()) continue; if(solution->evaluationContext.danger < danger) solution->evaluationContext.danger = danger; solution->evaluationContext.movementCost += path.movementCost(); #ifdef VCMI_TRACE_PATHFINDER logAi->trace("It's safe for %s to visit tile %s with danger %s, goal %s", hero->name, dest.toString(), std::to_string(danger), solution->name()); #endif result.push_back(solution); continue; } if(!armyValueRequired || armyValueRequired > danger) { armyValueRequired = boost::make_optional(danger); } } } danger = armyValueRequired.get_value_or(0); if(allowGatherArmy && danger > 0) { //we need to get army in order to conquer that place #ifdef VCMI_TRACE_PATHFINDER logAi->trace("Gather army for %s, value=%s", hero->name, std::to_string(danger)); #endif result.push_back(sptr(Goals::GatherArmy(danger * SAFE_ATTACK_CONSTANT).sethero(hero).setisAbstract(true))); } return result; } Goals::TSubgoal PathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet) { if(isBlockedBorderGate(firstTileToGet)) { //FIXME: this way we'll not visit gate and activate quest :? return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->subID)); } auto topObj = cb->getTopObj(firstTileToGet); if(topObj) { if(vstd::contains(ai->reservedObjs, topObj) && !vstd::contains(ai->reservedHeroesMap[hero], topObj)) { return sptr(Goals::Invalid()); } if(topObj->ID == Obj::HERO && cb->getPlayerRelations(hero->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES) { if(topObj != hero.get(true)) //the hero we want to free { logAi->error("%s stands in the way of %s", topObj->getObjectName(), hero->getObjectName()); return sptr(Goals::Invalid()); } } if(topObj->ID == Obj::QUEST_GUARD || topObj->ID == Obj::BORDERGUARD) { if(shouldVisit(hero, topObj)) { //do NOT use VISIT_TILE, as tile with quets guard can't be visited return sptr(Goals::VisitObj(topObj->id.getNum()).sethero(hero)); } auto questObj = dynamic_cast(topObj); if(questObj) { auto questInfo = QuestInfo(questObj->quest, topObj, topObj->visitablePos()); return sptr(Goals::CompleteQuest(questInfo)); } return sptr(Goals::Invalid()); } } return sptr(Goals::VisitTile(firstTileToGet).sethero(hero).setisAbstract(true)); } void PathfindingManager::resetPaths() { logAi->debug("AIPathfinder has been reseted."); pathfinder->clear(); } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/Pathfinding/PathfindingManager.h000066400000000000000000000041261342332007200243220ustar00rootroot00000000000000/* * PathfindingManager.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../VCAI.h" #include "AINodeStorage.h" class DLL_EXPORT IPathfindingManager { public: virtual ~IPathfindingManager() = default; virtual void init(CPlayerSpecificInfoCallback * CB) = 0; virtual void setAI(VCAI * AI) = 0; virtual void resetPaths() = 0; virtual Goals::TGoalVec howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy = true) = 0; virtual Goals::TGoalVec howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy = true) = 0; virtual Goals::TGoalVec howToVisitTile(int3 tile) = 0; virtual Goals::TGoalVec howToVisitObj(ObjectIdRef obj) = 0; virtual std::vector getPathsToTile(HeroPtr hero, int3 tile) = 0; }; class DLL_EXPORT PathfindingManager : public IPathfindingManager { friend class AIhelper; private: CPlayerSpecificInfoCallback * cb; //this is enough, but we downcast from CCallback VCAI * ai; std::unique_ptr pathfinder; public: PathfindingManager() = default; PathfindingManager(CPlayerSpecificInfoCallback * CB, VCAI * AI = nullptr); //for tests only Goals::TGoalVec howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy = true) override; Goals::TGoalVec howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy = true) override; Goals::TGoalVec howToVisitTile(int3 tile) override; Goals::TGoalVec howToVisitObj(ObjectIdRef obj) override; std::vector getPathsToTile(HeroPtr hero, int3 tile) override; void resetPaths() override; STRONG_INLINE bool isTileAccessible(const HeroPtr & hero, const int3 & tile) { return pathfinder->isTileAccessible(hero, tile); } private: void init(CPlayerSpecificInfoCallback * CB) override; void setAI(VCAI * AI) override; Goals::TGoalVec findPath( HeroPtr hero, crint3 dest, bool allowGatherArmy, const std::function goalFactory); Goals::TSubgoal clearWayTo(HeroPtr hero, int3 firstTileToGet); }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/ResourceManager.cpp000066400000000000000000000230761342332007200217630ustar00rootroot00000000000000/* * ResourceManager.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "ResourceManager.h" #include "Goals/Goals.h" #include "../../CCallback.h" #include "../../lib/mapObjects/MapObjects.h" #define GOLD_RESERVE (10000); //at least we'll be able to reach capitol ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal) : resources(Res), goal(Goal) { } bool ResourceObjective::operator<(const ResourceObjective & ro) const { return goal->priority < ro.goal->priority; } ResourceManager::ResourceManager(CPlayerSpecificInfoCallback * CB, VCAI * AI) : ai(AI), cb(CB) { } void ResourceManager::init(CPlayerSpecificInfoCallback * CB) { cb = CB; } void ResourceManager::setAI(VCAI * AI) { ai = AI; } bool ResourceManager::canAfford(const TResources & cost) const { return freeResources().canAfford(cost); } TResources ResourceManager::estimateIncome() const { TResources ret; for (const CGTownInstance * t : cb->getTownsInfo()) { ret += t->dailyIncome(); } for (const CGObjectInstance * obj : ai->getFlaggedObjects()) { if (obj->ID == Obj::MINE) { switch (obj->subID) { case Res::WOOD: case Res::ORE: ret[obj->subID] += WOOD_ORE_MINE_PRODUCTION; break; case Res::GOLD: case 7: //abandoned mine -> also gold ret[Res::GOLD] += GOLD_MINE_PRODUCTION; break; default: ret[obj->subID] += RESOURCE_MINE_PRODUCTION; break; } } } return ret; } void ResourceManager::reserveResoures(const TResources & res, Goals::TSubgoal goal) { if (!goal->invalid()) tryPush(ResourceObjective(res, goal)); else logAi->warn("Attempt to reserve resources for Invalid goal"); } Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o) const { auto allResources = cb->getResourceAmount(); auto income = estimateIncome(); Res::ERes resourceType = Res::INVALID; TResource amountToCollect = 0; typedef std::pair resPair; std::map missingResources; //TODO: unit test for complex resource sets //sum missing resources of given type for ALL reserved objectives for (auto it = queue.ordered_begin(); it != queue.ordered_end(); it++) { //choose specific resources we need for this goal (not 0) for (auto r = Res::ResourceSet::nziterator(o.resources); r.valid(); r++) missingResources[r->resType] += it->resources[r->resType]; //goal it costs r units of resType } for (auto it = Res::ResourceSet::nziterator(o.resources); it.valid(); it++) { missingResources[it->resType] -= allResources[it->resType]; //missing = (what we need) - (what we have) vstd::amax(missingResources[it->resType], 0); // if we have more resources than reserved, we don't need them } vstd::erase_if(missingResources, [=](const resPair & p) -> bool { return !(p.second); //in case evaluated to 0 or less }); if (missingResources.empty()) //FIXME: should be unit-tested out { logAi->error("We don't need to collect resources %s for goal %s", o.resources.toString(), o.goal->name()); return o.goal; } float goalPriority = 10; //arbitrary, will be divided for (const resPair & p : missingResources) { if (!income[p.first]) //prioritize resources with 0 income { resourceType = p.first; amountToCollect = p.second; goalPriority /= amountToCollect; //need more resources -> lower priority break; } } if (resourceType == Res::INVALID) //no needed resources has 0 income, { //find the one which takes longest to collect typedef std::pair timePair; std::map daysToEarn; for (auto it : missingResources) daysToEarn[it.first] = (float)missingResources[it.first] / income[it.first]; auto incomeComparer = [&income](const timePair & lhs, const timePair & rhs) -> bool { //theoretically income can be negative, but that falls into this comparison return lhs.second < rhs.second; }; resourceType = boost::max_element(daysToEarn, incomeComparer)->first; amountToCollect = missingResources[resourceType]; goalPriority /= daysToEarn[resourceType]; //more days - lower priority } if (resourceType == Res::GOLD) goalPriority *= 1000; //this is abstract goal and might take soem time to complete return Goals::sptr(Goals::CollectRes(resourceType, amountToCollect).setisAbstract(true)); } Goals::TSubgoal ResourceManager::whatToDo() const //suggest any goal { if (queue.size()) { auto o = queue.top(); auto allResources = cb->getResourceAmount(); //we don't consider savings, it's out top-priority goal if (allResources.canAfford(o.resources)) return o.goal; else //we can't afford even top-priority goal, need to collect resources return collectResourcesForOurGoal(o); } else return Goals::sptr(Goals::Invalid()); //nothing else to do } Goals::TSubgoal ResourceManager::whatToDo(TResources &res, Goals::TSubgoal goal) { logAi->trace("ResourceManager: checking goal %s which requires resources %s", goal->name(), res.toString()); TResources accumulatedResources; auto allResources = cb->getResourceAmount(); ResourceObjective ro(res, goal); tryPush(ro); //check if we can afford all the objectives with higher priority first for (auto it = queue.ordered_begin(); it != queue.ordered_end(); it++) { accumulatedResources += it->resources; logAi->trace( "ResourceManager: checking goal %s, accumulatedResources=%s, available=%s", it->goal->name(), accumulatedResources.toString(), allResources.toString()); if(!accumulatedResources.canBeAfforded(allResources)) { //can't afford break; } else //can afford all goals up to this point { if(it->goal == goal) { logAi->debug("ResourceManager: can afford goal %s", goal->name()); return goal; //can afford immediately } } } logAi->debug("ResourceManager: can not afford goal %s", goal->name()); return collectResourcesForOurGoal(ro); } bool ResourceManager::containsObjective(Goals::TSubgoal goal) const { logAi->trace("Entering ResourceManager.containsObjective goal=%s", goal->name()); dumpToLog(); //TODO: unit tests for once for (auto objective : queue) { if (objective.goal == goal) return true; } return false; } bool ResourceManager::notifyGoalCompleted(Goals::TSubgoal goal) { logAi->trace("Entering ResourceManager.notifyGoalCompleted goal=%s", goal->name()); if (goal->invalid()) logAi->warn("Attempt to complete Invalid goal"); std::function equivalentGoalsCheck = [goal](const Goals::TSubgoal & x) -> bool { return x == goal || x->fulfillsMe(goal); }; bool removedGoal = removeOutdatedObjectives(equivalentGoalsCheck); dumpToLog(); return removedGoal; } bool ResourceManager::updateGoal(Goals::TSubgoal goal) { //we update priority of goal if it is easier or more difficult to complete if (goal->invalid()) logAi->warn("Attempt to update Invalid goal"); auto it = boost::find_if(queue, [goal](const ResourceObjective & ro) -> bool { return ro.goal == goal; }); if (it != queue.end()) { it->goal->setpriority(goal->priority); auto handle = queue.s_handle_from_iterator(it); queue.update(handle); //restore order return true; } else return false; } void ResourceManager::dumpToLog() const { for(auto it = queue.ordered_begin(); it != queue.ordered_end(); it++) { logAi->trace("ResourceManager contains goal %s which requires resources %s", it->goal->name(), it->resources.toString()); } } bool ResourceManager::tryPush(const ResourceObjective & o) { auto goal = o.goal; logAi->trace("ResourceManager: Trying to add goal %s which requires resources %s", goal->name(), o.resources.toString()); dumpToLog(); auto it = boost::find_if(queue, [goal](const ResourceObjective & ro) -> bool { return ro.goal == goal; }); if (it != queue.end()) { auto handle = queue.s_handle_from_iterator(it); vstd::amax(goal->priority, it->goal->priority); //increase priority if case //update resources with new value queue.update(handle, ResourceObjective(o.resources, goal)); //restore order return false; } else { queue.push(o); //add new objective logAi->debug("Reserved resources (%s) for %s", o.resources.toString(), goal->name()); return true; } } bool ResourceManager::hasTasksLeft() const { return !queue.empty(); } bool ResourceManager::removeOutdatedObjectives(std::function predicate) { bool removedAnything = false; while(true) { //unfortunately we can't use remove_if on heap auto it = boost::find_if(queue, [&](const ResourceObjective & ro) -> bool { return predicate(ro.goal); }); if(it != queue.end()) //removed at least one { logAi->debug("Removing goal %s from ResourceManager.", it->goal->name()); queue.erase(queue.s_handle_from_iterator(it)); removedAnything = true; } else { //found nothing more to remove break; } } return removedAnything; } TResources ResourceManager::reservedResources() const { TResources res; for (auto it : queue) //substract the value of reserved goals res += it.resources; return res; } TResources ResourceManager::freeResources() const { TResources myRes = cb->getResourceAmount(); myRes -= reservedResources(); //substract the value of reserved goals for (auto & val : myRes) vstd::amax(val, 0); //never negative return myRes; } TResource ResourceManager::freeGold() const { return freeResources()[Res::GOLD]; } TResources ResourceManager::allResources() const { return cb->getResourceAmount(); } TResource ResourceManager::allGold() const { return cb->getResourceAmount()[Res::GOLD]; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/ResourceManager.h000066400000000000000000000073671342332007200214350ustar00rootroot00000000000000/* * ResourceManager.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "AIUtility.h" #include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" #include "VCAI.h" #include class AIhelper; class IResourceManager; struct DLL_EXPORT ResourceObjective { ResourceObjective() = default; ResourceObjective(const TResources &res, Goals::TSubgoal goal); bool operator < (const ResourceObjective &ro) const; TResources resources; //how many resoures do we need Goals::TSubgoal goal; //what for (build, gather army etc...) //TODO: register? template void serializeInternal(Handler & h, const int version) { h & resources; //h & goal; //FIXME: goal serialization is broken } }; class DLL_EXPORT IResourceManager //: public: IAbstractManager { public: virtual ~IResourceManager() = default; virtual void init(CPlayerSpecificInfoCallback * CB) = 0; virtual void setAI(VCAI * AI) = 0; virtual TResources reservedResources() const = 0; virtual TResources freeResources() const = 0; virtual TResource freeGold() const = 0; virtual TResources allResources() const = 0; virtual TResource allGold() const = 0; virtual Goals::TSubgoal whatToDo() const = 0;//get highest-priority goal virtual Goals::TSubgoal whatToDo(TResources &res, Goals::TSubgoal goal) = 0; virtual bool containsObjective(Goals::TSubgoal goal) const = 0; virtual bool hasTasksLeft() const = 0; virtual bool removeOutdatedObjectives(std::function predicate) = 0; //remove ResourceObjectives from queue if ResourceObjective->goal meets specific criteria virtual bool notifyGoalCompleted(Goals::TSubgoal goal) = 0; }; class DLL_EXPORT ResourceManager : public IResourceManager { /*Resource Manager is a smart shopping list for AI to help Uses priority queue based on CGoal.priority */ friend class VCAI; friend class AIhelper; friend struct SetGlobalState; CPlayerSpecificInfoCallback * cb; //this is enough, but we downcast from CCallback VCAI * ai; public: ResourceManager() = default; ResourceManager(CPlayerSpecificInfoCallback * CB, VCAI * AI = nullptr); //for tests only bool canAfford(const TResources & cost) const; TResources reservedResources() const override; TResources freeResources() const override; TResource freeGold() const override; TResources allResources() const override; TResource allGold() const override; Goals::TSubgoal whatToDo() const override; //peek highest-priority goal Goals::TSubgoal whatToDo(TResources & res, Goals::TSubgoal goal) override; //can we afford this goal or need to CollectRes? bool containsObjective(Goals::TSubgoal goal) const override; bool hasTasksLeft() const override; bool removeOutdatedObjectives(std::function predicate) override; bool notifyGoalCompleted(Goals::TSubgoal goal) override; protected: //not-const actions only for AI virtual void reserveResoures(const TResources & res, Goals::TSubgoal goal = Goals::TSubgoal()); virtual bool updateGoal(Goals::TSubgoal goal); //new goal must have same properties but different priority virtual bool tryPush(const ResourceObjective &o); //inner processing virtual TResources estimateIncome() const; virtual Goals::TSubgoal collectResourcesForOurGoal(ResourceObjective &o) const; void init(CPlayerSpecificInfoCallback * CB) override; void setAI(VCAI * AI) override; private: TResources saving; boost::heap::binomial_heap queue; void dumpToLog() const; //TODO: register? template void serializeInternal(Handler & h, const int version) { h & saving; h & queue; } }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/SectorMap.cpp000066400000000000000000000251461342332007200205760ustar00rootroot00000000000000/* * SectorMap.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "SectorMap.h" #include "VCAI.h" #include "../../CCallback.h" #include "../../lib/mapping/CMap.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/CPathfinder.h" #include "../../lib/CGameState.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; SectorMap::SectorMap() { update(); } SectorMap::SectorMap(HeroPtr h) { update(); makeParentBFS(h->visitablePos()); } bool SectorMap::markIfBlocked(TSectorID & sec, crint3 pos, const TerrainTile * t) { if (t->blocked && !t->visitable) { sec = NOT_AVAILABLE; return true; } return false; } bool SectorMap::markIfBlocked(TSectorID & sec, crint3 pos) { return markIfBlocked(sec, pos, getTile(pos)); } void SectorMap::update() { visibleTiles = cb->getAllVisibleTiles(); auto shape = visibleTiles->shape(); sector.resize(boost::extents[shape[0]][shape[1]][shape[2]]); clear(); int curSector = 3; //0 is invisible, 1 is not explored CCallback * cbp = cb.get(); //optimization foreach_tile_pos([&](crint3 pos) { if (retrieveTile(pos) == NOT_CHECKED) { if (!markIfBlocked(retrieveTile(pos), pos)) exploreNewSector(pos, curSector++, cbp); } }); valid = true; } SectorMap::TSectorID & SectorMap::retrieveTileN(SectorMap::TSectorArray & a, const int3 & pos) { return a[pos.x][pos.y][pos.z]; } const SectorMap::TSectorID & SectorMap::retrieveTileN(const SectorMap::TSectorArray & a, const int3 & pos) { return a[pos.x][pos.y][pos.z]; } void SectorMap::clear() { //TODO: rotate to [z][x][y] auto fow = cb->getVisibilityMap(); //TODO: any magic to automate this? will need array->array conversion //std::transform(fow.begin(), fow.end(), sector.begin(), [](const ui8 &f) -> unsigned short //{ // return f; //type conversion //}); auto width = fow.size(); auto height = fow.front().size(); auto depth = fow.front().front().size(); for (size_t x = 0; x < width; x++) { for (size_t y = 0; y < height; y++) { for (size_t z = 0; z < depth; z++) sector[x][y][z] = fow[x][y][z]; } } valid = false; } void SectorMap::exploreNewSector(crint3 pos, int num, CCallback * cbp) { Sector & s = infoOnSectors[num]; s.id = num; s.water = getTile(pos)->isWater(); std::queue toVisit; toVisit.push(pos); while (!toVisit.empty()) { int3 curPos = toVisit.front(); toVisit.pop(); TSectorID & sec = retrieveTile(curPos); if (sec == NOT_CHECKED) { const TerrainTile * t = getTile(curPos); if (!markIfBlocked(sec, curPos, t)) { if (t->isWater() == s.water) //sector is only-water or only-land { sec = num; s.tiles.push_back(curPos); foreach_neighbour(cbp, curPos, [&](CCallback * cbp, crint3 neighPos) { if (retrieveTile(neighPos) == NOT_CHECKED) { toVisit.push(neighPos); //parent[neighPos] = curPos; } const TerrainTile * nt = getTile(neighPos); if (nt && nt->isWater() != s.water && canBeEmbarkmentPoint(nt, s.water)) { s.embarkmentPoints.push_back(neighPos); } }); if (t->visitable) { auto obj = t->visitableObjects.front(); if (cb->getObj(obj->id, false)) // FIXME: we have to filter invisible objcts like events, but probably TerrainTile shouldn't be used in SectorMap at all s.visitableObjs.push_back(obj); } } } } } vstd::removeDuplicates(s.embarkmentPoints); } void SectorMap::write(crstring fname) { std::ofstream out(fname); for (int k = 0; k < cb->getMapSize().z; k++) { for (int j = 0; j < cb->getMapSize().y; j++) { for (int i = 0; i < cb->getMapSize().x; i++) { out << (int)sector[i][j][k] << '\t'; } out << std::endl; } out << std::endl; } } /* this functions returns one target tile or invalid tile. We will use it to poll possible destinations For ship construction etc, another function (goal?) is needed */ int3 SectorMap::firstTileToGet(HeroPtr h, crint3 dst) { int3 ret(-1, -1, -1); int sourceSector = retrieveTile(h->visitablePos()); int destinationSector = retrieveTile(dst); const Sector * src = &infoOnSectors[sourceSector]; const Sector * dest = &infoOnSectors[destinationSector]; if (sourceSector != destinationSector) //use ships, shipyards etc.. { if (ai->isAccessibleForHero(dst, h)) //pathfinder can find a way using ships and gates if tile is not blocked by objects return dst; std::map preds; std::queue sectorQueue; sectorQueue.push(src); while (!sectorQueue.empty()) { const Sector * s = sectorQueue.front(); sectorQueue.pop(); for (int3 ep : s->embarkmentPoints) { Sector * neigh = &infoOnSectors[retrieveTile(ep)]; //preds[s].push_back(neigh); if (!preds[neigh]) { preds[neigh] = s; sectorQueue.push(neigh); } } } if (!preds[dest]) { //write("test.txt"); return ret; //throw cannotFulfillGoalException(boost::str(boost::format("Cannot find connection between sectors %d and %d") % src->id % dst->id)); } std::vector toTraverse; toTraverse.push_back(dest); while (toTraverse.back() != src) { toTraverse.push_back(preds[toTraverse.back()]); } if (preds[dest]) { //TODO: would be nice to find sectors in loop const Sector * sectorToReach = toTraverse.at(toTraverse.size() - 2); if (!src->water && sectorToReach->water) //embark { //embark on ship -> look for an EP with a boat auto firstEP = boost::find_if(src->embarkmentPoints, [=](crint3 pos) -> bool { const TerrainTile * t = getTile(pos); if (t && t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT) { if (retrieveTile(pos) == sectorToReach->id) return true; } return false; }); if (firstEP != src->embarkmentPoints.end()) { return *firstEP; } else { //we need to find a shipyard with an access to the desired sector's EP //TODO what about Summon Boat spell? std::vector shipyards; for (const CGTownInstance * t : cb->getTownsInfo()) { if (t->hasBuilt(BuildingID::SHIPYARD)) shipyards.push_back(t); } for (const CGObjectInstance * obj : ai->getFlaggedObjects()) { if (obj->ID != Obj::TOWN) //towns were handled in the previous loop { if (const IShipyard * shipyard = IShipyard::castFrom(obj)) shipyards.push_back(shipyard); } } shipyards.erase(boost::remove_if(shipyards, [=](const IShipyard * shipyard) -> bool { return shipyard->shipyardStatus() != 0 || retrieveTile(shipyard->bestLocation()) != sectorToReach->id; }), shipyards.end()); if (!shipyards.size()) { //TODO consider possibility of building shipyard in a town return ret; //throw cannotFulfillGoalException("There is no known shipyard!"); } //we have only shipyards that possibly can build ships onto the appropriate EP auto ownedGoodShipyard = boost::find_if(shipyards, [](const IShipyard * s) -> bool { return s->o->tempOwner == ai->playerID; }); if (ownedGoodShipyard != shipyards.end()) { const IShipyard * s = *ownedGoodShipyard; TResources shipCost; s->getBoatCost(shipCost); if (cb->getResourceAmount().canAfford(shipCost)) { int3 ret = s->bestLocation(); cb->buildBoat(s); //TODO: move actions elsewhere return ret; } else { //TODO gather res return ret; //throw cannotFulfillGoalException("Not enough resources to build a boat"); } } else { //TODO pick best shipyard to take over return shipyards.front()->o->visitablePos(); } } } else if (src->water && !sectorToReach->water) { //TODO //disembark return ret; } else //use subterranean gates - not needed since gates are now handled via Pathfinder { return ret; //throw cannotFulfillGoalException("Land-land and water-water inter-sector transitions are not implemented!"); } } else { return ret; //throw cannotFulfillGoalException("Inter-sector route detection failed: not connected sectors?"); } } else //tiles are in same sector { return findFirstVisitableTile(h, dst); } } int3 SectorMap::findFirstVisitableTile(HeroPtr h, crint3 dst) { int3 ret(-1, -1, -1); int3 curtile = dst; while (curtile != h->visitablePos()) { auto topObj = cb->getTopObj(curtile); if (topObj && topObj->ID == Obj::HERO && topObj != h.h) { if (cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES) { logAi->warn("Another allied hero stands in our way"); return ret; } } if (ai->myCb->getPathsInfo(h.get())->getPathInfo(curtile)->reachable()) { return curtile; } else { auto i = parent.find(curtile); if (i != parent.end()) { assert(curtile != i->second); curtile = i->second; } else { return ret; //throw cannotFulfillGoalException("Unreachable tile in sector? Should not happen!"); } } } return ret; } void SectorMap::makeParentBFS(crint3 source) { parent.clear(); int mySector = retrieveTile(source); std::queue toVisit; toVisit.push(source); while (!toVisit.empty()) { int3 curPos = toVisit.front(); toVisit.pop(); TSectorID & sec = retrieveTile(curPos); assert(sec == mySector); //consider only tiles from the same sector UNUSED(sec); foreach_neighbour(curPos, [&](crint3 neighPos) { if (retrieveTile(neighPos) == mySector && !vstd::contains(parent, neighPos)) { if (cb->canMoveBetween(curPos, neighPos)) { toVisit.push(neighPos); parent[neighPos] = curPos; } } }); } } SectorMap::TSectorID & SectorMap::retrieveTile(crint3 pos) { return retrieveTileN(sector, pos); } TerrainTile * SectorMap::getTile(crint3 pos) const { //out of bounds access should be handled by boost::multi_array //still we cached this array to avoid any checks return visibleTiles->operator[](pos.x)[pos.y][pos.z]; } std::vector SectorMap::getNearbyObjs(HeroPtr h, bool sectorsAround) { const Sector * heroSector = &infoOnSectors[retrieveTile(h->visitablePos())]; if (sectorsAround) { std::vector ret; for (auto embarkPoint : heroSector->embarkmentPoints) { const Sector * embarkSector = &infoOnSectors[retrieveTile(embarkPoint)]; range::copy(embarkSector->visitableObjs, std::back_inserter(ret)); } return ret; } return heroSector->visitableObjs; }vcmi-0.99+git20190113.f06c8a87/AI/VCAI/SectorMap.h000066400000000000000000000040011342332007200202260ustar00rootroot00000000000000/* * SectorMap.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "AIUtility.h" enum { NOT_VISIBLE = 0, NOT_CHECKED = 1, NOT_AVAILABLE }; struct SectorMap { //a sector is set of tiles that would be mutually reachable if all visitable objs would be passable (incl monsters) struct Sector { int id; std::vector tiles; std::vector embarkmentPoints; //tiles of other sectors onto which we can (dis)embark std::vector visitableObjs; bool water; //all tiles of sector are land or water Sector() { id = -1; water = false; } }; typedef unsigned short TSectorID; //smaller than int to allow -1 value. Max number of sectors 65K should be enough for any proper map. typedef boost::multi_array TSectorArray; bool valid; //some kind of lazy eval std::map parent; TSectorArray sector; //std::vector>> pathfinderSector; std::map infoOnSectors; std::shared_ptr> visibleTiles; SectorMap(); SectorMap(HeroPtr h); void update(); void clear(); void exploreNewSector(crint3 pos, int num, CCallback * cbp); void write(crstring fname); bool markIfBlocked(TSectorID & sec, crint3 pos, const TerrainTile * t); bool markIfBlocked(TSectorID & sec, crint3 pos); TSectorID & retrieveTile(crint3 pos); TSectorID & retrieveTileN(TSectorArray & vectors, const int3 & pos); const TSectorID & retrieveTileN(const TSectorArray & vectors, const int3 & pos); TerrainTile * getTile(crint3 pos) const; std::vector getNearbyObjs(HeroPtr h, bool sectorsAround); void makeParentBFS(crint3 source); int3 firstTileToGet(HeroPtr h, crint3 dst); //if h wants to reach tile dst, which tile he should visit to clear the way? int3 findFirstVisitableTile(HeroPtr h, crint3 dst); }; vcmi-0.99+git20190113.f06c8a87/AI/VCAI/StdInc.cpp000066400000000000000000000000251342332007200200520ustar00rootroot00000000000000#include "StdInc.h" vcmi-0.99+git20190113.f06c8a87/AI/VCAI/StdInc.h000066400000000000000000000000521342332007200175170ustar00rootroot00000000000000#pragma once #include "../../Global.h" vcmi-0.99+git20190113.f06c8a87/AI/VCAI/VCAI.cbp000066400000000000000000000140631342332007200174010ustar00rootroot00000000000000 vcmi-0.99+git20190113.f06c8a87/AI/VCAI/VCAI.cpp000066400000000000000000002462631342332007200174300ustar00rootroot00000000000000/* * VCAI.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "VCAI.h" #include "FuzzyHelper.h" #include "ResourceManager.h" #include "BuildingManager.h" #include "Goals/Goals.h" #include "../../lib/UnlockGuard.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/CModHandler.h" #include "../../lib/CGameState.h" #include "../../lib/NetPacks.h" #include "../../lib/serializer/CTypeList.h" #include "../../lib/serializer/BinarySerializer.h" #include "../../lib/serializer/BinaryDeserializer.h" #include "AIhelper.h" extern FuzzyHelper * fh; class CGVisitableOPW; const double SAFE_ATTACK_CONSTANT = 1.5; //one thread may be turn of AI and another will be handling a side effect for AI2 boost::thread_specific_ptr cb; boost::thread_specific_ptr ai; //std::map > HeroView::infosCount; //helper RAII to manage global ai/cb ptrs struct SetGlobalState { SetGlobalState(VCAI * AI) { assert(!ai.get()); assert(!cb.get()); ai.reset(AI); cb.reset(AI->myCb.get()); } ~SetGlobalState() { //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully //TODO: to ensure that, make rm unique_ptr ai.release(); cb.release(); } }; #define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai); #define NET_EVENT_HANDLER SET_GLOBAL_STATE(this) #define MAKING_TURN SET_GLOBAL_STATE(this) VCAI::VCAI() { LOG_TRACE(logAi); makingTurn = nullptr; destinationTeleport = ObjectInstanceID(); destinationTeleportPos = int3(-1); ah = new AIhelper(); ah->setAI(this); } VCAI::~VCAI() { delete ah; LOG_TRACE(logAi); finish(); } void VCAI::availableCreaturesChanged(const CGDwelling * town) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::heroMoved(const TryMoveHero & details) { LOG_TRACE(logAi); NET_EVENT_HANDLER; validateObject(details.id); //enemy hero may have left visible area auto hero = cb->getHero(details.id); ah->resetPaths(); const int3 from = CGHeroInstance::convertPosition(details.start, false); const int3 to = CGHeroInstance::convertPosition(details.end, false); const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from)); const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to)); if(details.result == TryMoveHero::TELEPORTATION) { auto t1 = dynamic_cast(o1); auto t2 = dynamic_cast(o2); if(t1 && t2) { if(cb->isTeleportChannelBidirectional(t1->channel)) { if(o1->ID == Obj::SUBTERRANEAN_GATE && o1->ID == o2->ID) // We need to only add subterranean gates in knownSubterraneanGates. Used for features not yet ported to use teleport channels { knownSubterraneanGates[o1] = o2; knownSubterraneanGates[o2] = o1; logAi->debug("Found a pair of subterranean gates between %s and %s!", from.toString(), to.toString()); } } } //FIXME: teleports are not correctly visited unreserveObject(hero, t1); unreserveObject(hero, t2); } else if(details.result == TryMoveHero::EMBARK && hero) { //make sure AI not attempt to visit used boat validateObject(hero->boat); } else if(details.result == TryMoveHero::DISEMBARK && o1) { auto boat = dynamic_cast(o1); if(boat) addVisitableObj(boat); } } void VCAI::heroInGarrisonChange(const CGTownInstance * town) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::centerView(int3 pos, int focusTime) { LOG_TRACE_PARAMS(logAi, "focusTime '%i'", focusTime); NET_EVENT_HANDLER; } void VCAI::artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::artifactAssembled(const ArtifactLocation & al) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::showTavernWindow(const CGObjectInstance * townOrTavern) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::showThievesGuildWindow(const CGObjectInstance * obj) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::playerBlocked(int reason, bool start) { LOG_TRACE_PARAMS(logAi, "reason '%i', start '%i'", reason % start); NET_EVENT_HANDLER; if(start && reason == PlayerBlocked::UPCOMING_BATTLE) status.setBattle(UPCOMING_BATTLE); if(reason == PlayerBlocked::ONGOING_MOVEMENT) status.setMove(start); } void VCAI::showPuzzleMap() { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::showShipyardDialog(const IShipyard * obj) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) { LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf); NET_EVENT_HANDLER; logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.getStr(), player, player.getStr(), (victoryLossCheckResult.victory() ? "won" : "lost")); if(player == playerID) { if(victoryLossCheckResult.victory()) { logAi->debug("VCAI: I won! Incredible!"); logAi->debug("Turn nr %d", myCb->getDate()); } else { logAi->debug("VCAI: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.getStr()); } finish(); } } void VCAI::artifactPut(const ArtifactLocation & al) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::artifactRemoved(const ArtifactLocation & al) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::artifactDisassembled(const ArtifactLocation & al) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) { LOG_TRACE_PARAMS(logAi, "start '%i'; obj '%s'", start % (visitedObj ? visitedObj->getObjectName() : std::string("n/a"))); NET_EVENT_HANDLER; if(start && visitedObj) //we can end visit with null object, anyway { markObjectVisited(visitedObj); unreserveObject(visitor, visitedObj); completeGoal(sptr(Goals::VisitObj(visitedObj->id.getNum()).sethero(visitor))); //we don't need to visit it anymore //TODO: what if we visited one-time visitable object that was reserved by another hero (shouldn't, but..) if (visitedObj->ID == Obj::HERO) { visitedHeroes[visitor].insert(HeroPtr(dynamic_cast(visitedObj))); } } status.heroVisit(visitedObj, start); } void VCAI::availableArtifactsChanged(const CGBlackMarket * bm) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) { LOG_TRACE(logAi); NET_EVENT_HANDLER; //buildArmyIn(town); //moveCreaturesToHero(town); } void VCAI::tileHidden(const std::unordered_set & pos) { LOG_TRACE(logAi); NET_EVENT_HANDLER; validateVisitableObjs(); clearPathsInfo(); } void VCAI::tileRevealed(const std::unordered_set & pos) { LOG_TRACE(logAi); NET_EVENT_HANDLER; for(int3 tile : pos) { for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile)) addVisitableObj(obj); } clearPathsInfo(); } void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) { LOG_TRACE(logAi); NET_EVENT_HANDLER; auto firstHero = cb->getHero(hero1); auto secondHero = cb->getHero(hero2); status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->name % firstHero->tempOwner % secondHero->name % secondHero->tempOwner)); requestActionASAP([=]() { float goalpriority1 = 0, goalpriority2 = 0; auto firstGoal = getGoal(firstHero); if(firstGoal->goalType == Goals::GATHER_ARMY) goalpriority1 = firstGoal->priority; auto secondGoal = getGoal(secondHero); if(secondGoal->goalType == Goals::GATHER_ARMY) goalpriority2 = secondGoal->priority; auto transferFrom2to1 = [this](const CGHeroInstance * h1, const CGHeroInstance * h2) -> void { this->pickBestCreatures(h1, h2); this->pickBestArtifacts(h1, h2); }; //Do not attempt army or artifacts exchange if we visited ally player //Visits can still be useful if hero have skills like Scholar if(firstHero->tempOwner != secondHero->tempOwner) { logAi->debug("Heroes owned by different players. Do not exchange army or artifacts."); } else if(goalpriority1 > goalpriority2) { transferFrom2to1(firstHero, secondHero); } else if(goalpriority1 < goalpriority2) { transferFrom2to1(secondHero, firstHero); } else //regular criteria { if(firstHero->getFightingStrength() > secondHero->getFightingStrength() && canGetArmy(firstHero, secondHero)) transferFrom2to1(firstHero, secondHero); else if(canGetArmy(secondHero, firstHero)) transferFrom2to1(secondHero, firstHero); } completeGoal(sptr(Goals::VisitHero(firstHero->id.getNum()))); //TODO: what if we were visited by other hero in the meantime? completeGoal(sptr(Goals::VisitHero(secondHero->id.getNum()))); answerQuery(query, 0); }); } void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) { LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", which % val); NET_EVENT_HANDLER; } void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) { LOG_TRACE_PARAMS(logAi, "level '%i'", level); NET_EVENT_HANDLER; } void VCAI::heroMovePointsChanged(const CGHeroInstance * hero) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::newObject(const CGObjectInstance * obj) { LOG_TRACE(logAi); NET_EVENT_HANDLER; if(obj->isVisitable()) addVisitableObj(obj); ah->resetPaths(); } //to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight //see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes void VCAI::objectRemoved(const CGObjectInstance * obj) { LOG_TRACE(logAi); NET_EVENT_HANDLER; vstd::erase_if_present(visitableObjs, obj); vstd::erase_if_present(alreadyVisited, obj); for(auto h : cb->getHeroesInfo()) unreserveObject(h, obj); std::function checkRemovalValidity = [&](const Goals::TSubgoal & x) -> bool { if((x->goalType == Goals::VISIT_OBJ) && (x->objid == obj->id.getNum())) return true; else if(x->parent && checkRemovalValidity(x->parent)) //repeat this lambda check recursively on parent goal return true; else return false; }; //clear VCAI / main loop caches vstd::erase_if(lockedHeroes, [&](const std::pair & x) -> bool { return checkRemovalValidity(x.second); }); vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool { return checkRemovalValidity(x.first); }); vstd::erase_if(basicGoals, checkRemovalValidity); vstd::erase_if(goalsToAdd, checkRemovalValidity); vstd::erase_if(goalsToRemove, checkRemovalValidity); for(auto goal : ultimateGoalsFromBasic) vstd::erase_if(goal.second, checkRemovalValidity); //clear resource manager goal cache ah->removeOutdatedObjectives(checkRemovalValidity); //TODO: Find better way to handle hero boat removal if(auto hero = dynamic_cast(obj)) { if(hero->boat) { vstd::erase_if_present(visitableObjs, hero->boat); vstd::erase_if_present(alreadyVisited, hero->boat); for(auto h : cb->getHeroesInfo()) unreserveObject(h, hero->boat); } } ah->resetPaths(); //TODO //there are other places where CGObjectinstance ptrs are stored... // if(obj->ID == Obj::HERO && obj->tempOwner == playerID) { lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion } } void VCAI::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) { LOG_TRACE(logAi); NET_EVENT_HANDLER; requestActionASAP([=]() { makePossibleUpgrades(visitor); }); } void VCAI::playerBonusChanged(const Bonus & bonus, bool gain) { LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); NET_EVENT_HANDLER; } void VCAI::heroCreated(const CGHeroInstance * h) { LOG_TRACE(logAi); if(h->visitedTown) townVisitsThisWeek[HeroPtr(h)].insert(h->visitedTown); NET_EVENT_HANDLER; } void VCAI::advmapSpellCast(const CGHeroInstance * caster, int spellID) { LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID); NET_EVENT_HANDLER; } void VCAI::showInfoDialog(const std::string & text, const std::vector & components, int soundID) { LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID); NET_EVENT_HANDLER; } void VCAI::requestRealized(PackageApplied * pa) { LOG_TRACE(logAi); NET_EVENT_HANDLER; if(status.haveTurn()) { if(pa->packType == typeList.getTypeID()) { if(pa->result) status.madeTurn(); } } if(pa->packType == typeList.getTypeID()) { status.receivedAnswerConfirmation(pa->requestID, pa->result); } } void VCAI::receivedResource() { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::heroManaPointsChanged(const CGHeroInstance * hero) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) { LOG_TRACE_PARAMS(logAi, "which '%d', val '%d'", which % val); NET_EVENT_HANDLER; } void VCAI::battleResultsApplied() { LOG_TRACE(logAi); NET_EVENT_HANDLER; assert(status.getBattle() == ENDING_BATTLE); status.setBattle(NO_BATTLE); } void VCAI::objectPropertyChanged(const SetObjectProperty * sop) { LOG_TRACE(logAi); NET_EVENT_HANDLER; if(sop->what == ObjProperty::OWNER) { if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES) { //we want to visit objects owned by oppponents auto obj = myCb->getObj(sop->id, false); if(obj) { addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set vstd::erase_if_present(alreadyVisited, obj); } } } } void VCAI::buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) { LOG_TRACE_PARAMS(logAi, "what '%i'", what); NET_EVENT_HANDLER; if(town->getOwner() == playerID && what == 1) //built completeGoal(sptr(Goals::BuildThis(buildingID, town))); } void VCAI::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) { LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); NET_EVENT_HANDLER; } void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::showWorldViewEx(const std::vector & objectPositions) { //TODO: AI support for ViewXXX spell LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::init(std::shared_ptr CB) { LOG_TRACE(logAi); myCb = CB; cbc = CB; ah->init(CB.get()); NET_EVENT_HANDLER; //sets ah->rm->cb playerID = *myCb->getMyColor(); myCb->waitTillRealize = true; myCb->unlockGsWhenWaiting = true; if(!fh) fh = new FuzzyHelper(); retrieveVisitableObjs(); } void VCAI::yourTurn() { LOG_TRACE(logAi); NET_EVENT_HANDLER; status.startedTurn(); makingTurn = make_unique(&VCAI::makeTurn, this); } void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) { LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); NET_EVENT_HANDLER; status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->name % hero->level)); requestActionASAP([=](){ answerQuery(queryID, 0); }); } void VCAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) { LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); NET_EVENT_HANDLER; status.addQuery(queryID, boost::str(boost::format("Commander %s of %s got level %d") % commander->name % commander->armyObj->nodeName() % (int)commander->level)); requestActionASAP([=](){ answerQuery(queryID, 0); }); } void VCAI::showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) { LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel); NET_EVENT_HANDLER; int sel = 0; status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s") % components.size() % text)); if(selection) //select from multiple components -> take the last one (they're indexed [1-size]) sel = components.size(); if(!selection && cancel) //yes&no -> always answer yes, we are a brave AI :) sel = 1; requestActionASAP([=]() { answerQuery(askID, sel); }); } void VCAI::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { // LOG_TRACE_PARAMS(logAi, "askID '%i', exits '%s'", askID % exits); NET_EVENT_HANDLER; status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") % exits.size())); int choosenExit = -1; if(impassable) { knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE; } else if(destinationTeleport != ObjectInstanceID() && destinationTeleportPos.valid()) { auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) choosenExit = vstd::find_pos(exits, neededExit); } for(auto exit : exits) { if(status.channelProbing() && exit.first == destinationTeleport) { choosenExit = vstd::find_pos(exits, exit); break; } else { // TODO: Implement checking if visiting that teleport will uncovert any FoW // So far this is the best option to handle decision about probing auto obj = cb->getObj(exit.first, false); if(obj == nullptr && !vstd::contains(teleportChannelProbingList, exit.first)) { if(exit.first != destinationTeleport) teleportChannelProbingList.push_back(exit.first); } } } requestActionASAP([=]() { answerQuery(askID, choosenExit); }); } void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) { LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID); NET_EVENT_HANDLER; std::string s1 = up ? up->nodeName() : "NONE"; std::string s2 = down ? down->nodeName() : "NONE"; status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2)); //you can't request action from action-response thread requestActionASAP([=]() { if(removableUnits) pickBestCreatures(down, up); answerQuery(queryID, 0); }); } void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) { NET_EVENT_HANDLER; status.addQuery(askID, "Map object select query"); requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); }); } void VCAI::saveGame(BinarySerializer & h, const int version) { LOG_TRACE_PARAMS(logAi, "version '%i'", version); NET_EVENT_HANDLER; validateVisitableObjs(); #if 0 //disabled due to issue 2890 registerGoals(h); #endif // 0 CAdventureAI::saveGame(h, version); serializeInternal(h, version); } void VCAI::loadGame(BinaryDeserializer & h, const int version) { LOG_TRACE_PARAMS(logAi, "version '%i'", version); //NET_EVENT_HANDLER; #if 0 //disabled due to issue 2890 registerGoals(h); #endif // 0 CAdventureAI::loadGame(h, version); serializeInternal(h, version); } void makePossibleUpgrades(const CArmedInstance * obj) { if(!obj) return; for(int i = 0; i < GameConstants::ARMY_SIZE; i++) { if(const CStackInstance * s = obj->getStackPtr(SlotID(i))) { UpgradeInfo ui; cb->getUpgradeInfo(obj, SlotID(i), ui); if(ui.oldID >= 0 && cb->getResourceAmount().canAfford(ui.cost[0] * s->count)) { cb->upgradeCreature(obj, SlotID(i), ui.newID[0]); } } } } void VCAI::makeTurn() { MAKING_TURN; auto day = cb->getDate(Date::EDateType::DAY); logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day); boost::shared_lock gsLock(CGameState::mutex); setThreadName("VCAI::makeTurn"); switch(cb->getDate(Date::DAY_OF_WEEK)) { case 1: { townVisitsThisWeek.clear(); std::vector objs; retrieveVisitableObjs(objs, true); for(const CGObjectInstance * obj : objs) { if(isWeeklyRevisitable(obj)) { addVisitableObj(obj); vstd::erase_if_present(alreadyVisited, obj); } } break; } } markHeroAbleToExplore(primaryHero()); visitedHeroes.clear(); ai->ah->resetPaths(); try { //it looks messy here, but it's better to have armed heroes before attempting realizing goals for (const CGTownInstance * t : cb->getTownsInfo()) moveCreaturesToHero(t); mainLoop(); /*Below function is also responsible for hero movement via internal wander function. By design it is separate logic for heroes that have nothing to do. Heroes that were not picked by striveToGoal(sptr(Goals::Win())); recently (so they do not have new goals and cannot continue/reevaluate previously locked goals) will do logic in wander().*/ performTypicalActions(); //for debug purpose for (auto h : cb->getHeroesInfo()) { if (h->movement) logAi->warn("Hero %s has %d MP left", h->name, h->movement); } } catch (boost::thread_interrupted & e) { logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn."); return; } catch (std::exception & e) { logAi->debug("Making turn thread has caught an exception: %s", e.what()); } endTurn(); } void VCAI::mainLoop() { std::vector elementarGoals; //no duplicates allowed (operator ==) basicGoals.clear(); validateVisitableObjs(); //get all potential and saved goals //TODO: not lose basicGoals.push_back(sptr(Goals::Win())); for (auto goalPair : lockedHeroes) { fh->setPriority(goalPair.second); //re-evaluate, as heroes moved in the meantime basicGoals.push_back(goalPair.second); } if (ah->hasTasksLeft()) basicGoals.push_back(ah->whatToDo()); for (auto quest : myCb->getMyQuests()) { basicGoals.push_back(sptr(Goals::CompleteQuest(quest))); } basicGoals.push_back(sptr(Goals::Build())); invalidPathHeroes.clear(); while (basicGoals.size()) { vstd::removeDuplicates(basicGoals); //TODO: container which does this automagically without has would be nice goalsToAdd.clear(); goalsToRemove.clear(); elementarGoals.clear(); ultimateGoalsFromBasic.clear(); logAi->debug("Main loop: decomposing %i basic goals", basicGoals.size()); for (auto basicGoal : basicGoals) { logAi->debug("Main loop: decomposing basic goal %s", basicGoal->name()); auto goalToDecompose = basicGoal; Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); int maxAbstractGoals = 10; while (!elementarGoal->isElementar && maxAbstractGoals) { try { elementarGoal = decomposeGoal(goalToDecompose); } catch (goalFulfilledException & e) { //it is impossible to continue some goals (like exploration, for example) //complete abstract goal for now, but maybe main goal finds another path logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); completeGoal(e.goal); //put in goalsToRemove break; } catch(cannotFulfillGoalException & e) { //it is impossible to continue some goals (like exploration, for example) //complete abstract goal for now, but maybe main goal finds another path goalsToRemove.push_back(basicGoal); logAi->debug("Goal %s decomposition failed: %s", goalToDecompose->name(), e.what()); break; } catch (std::exception & e) //decomposition failed, which means we can't decompose entire tree { goalsToRemove.push_back(basicGoal); logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); break; } if (elementarGoal->isAbstract) //we can decompose it further { goalsToAdd.push_back(elementarGoal); //decompose further now - this is necesssary if we can't add over 10 goals in the pool goalToDecompose = elementarGoal; //there is a risk of infinite abstract goal loop, though it indicates failed logic maxAbstractGoals--; } else if (elementarGoal->isElementar) //should be { logAi->debug("Found elementar goal %s", elementarGoal->name()); elementarGoals.push_back(elementarGoal); ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? break; } else //should never be here throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); } } logAi->trace("Main loop: selecting best elementar goal"); //now choose one elementar goal to realize Goals::TGoalVec possibleGoals(elementarGoals.begin(), elementarGoals.end()); //copy to vector Goals::TSubgoal goalToRealize = sptr(Goals::Invalid()); while (possibleGoals.size()) { //allow assign goals to heroes with 0 movement, but don't realize them //maybe there are beter ones left auto bestGoal = fh->chooseSolution(possibleGoals); if (bestGoal->hero) //lock this hero to fulfill goal { setGoal(bestGoal->hero, bestGoal); if (!bestGoal->hero->movement || vstd::contains(invalidPathHeroes, bestGoal->hero)) { if (!vstd::erase_if_present(possibleGoals, bestGoal)) { logAi->error("erase_if_preset failed? Something very wrong!"); break; } continue; //chose next from the list } } goalToRealize = bestGoal; //we found our goal to execute break; } //realize best goal if (!goalToRealize->invalid()) { logAi->debug("Trying to realize %s (value %2.3f)", goalToRealize->name(), goalToRealize->priority); try { boost::this_thread::interruption_point(); goalToRealize->accept(this); //visitor pattern boost::this_thread::interruption_point(); } catch (boost::thread_interrupted & e) { logAi->debug("Player %d: Making turn thread received an interruption!", playerID); throw; //rethrow, we want to truly end this thread } catch (goalFulfilledException & e) { //the sub-goal was completed successfully completeGoal(e.goal); //local goal was also completed? completeGoal(goalToRealize); // remove abstract visit tile if we completed the elementar one vstd::erase_if_present(goalsToAdd, goalToRealize); } catch (std::exception & e) { logAi->debug("Failed to realize subgoal of type %s, I will stop.", goalToRealize->name()); logAi->debug("The error message was: %s", e.what()); //erase base goal if we failed to execute decomposed goal for (auto basicGoal : ultimateGoalsFromBasic[goalToRealize]) goalsToRemove.push_back(basicGoal); // sometimes resource manager contains an elementar goal which is not able to execute anymore and just fails each turn. ai->ah->notifyGoalCompleted(goalToRealize); //we failed to realize best goal, but maybe others are still possible? } //remove goals we couldn't decompose for (auto goal : goalsToRemove) vstd::erase_if_present(basicGoals, goal); //add abstract goals boost::sort(goalsToAdd, [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool { return lhs->priority > rhs->priority; //highest priority at the beginning }); //max number of goals = 10 int i = 0; while (basicGoals.size() < 10 && goalsToAdd.size() > i) { if (!vstd::contains(basicGoals, goalsToAdd[i])) //don't add duplicates basicGoals.push_back(goalsToAdd[i]); i++; } } else //no elementar goals possible { logAi->debug("Goal decomposition exhausted"); break; } } } void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h) { LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->name % obj->getObjectName() % obj->pos.toString()); switch(obj->ID) { case Obj::CREATURE_GENERATOR1: recruitCreatures(dynamic_cast(obj), h.get()); checkHeroArmy(h); break; case Obj::TOWN: moveCreaturesToHero(dynamic_cast(obj)); if(h->visitedTown) //we are inside, not just attacking { townVisitsThisWeek[h].insert(h->visitedTown); if(!h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST) { if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1)) cb->buyArtifact(h.get(), ArtifactID::SPELLBOOK); } } break; } completeGoal(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h))); } void VCAI::moveCreaturesToHero(const CGTownInstance * t) { if(t->visitingHero && t->armedGarrison() && t->visitingHero->tempOwner == t->tempOwner) { pickBestCreatures(t->visitingHero, t); } } bool VCAI::canGetArmy(const CGHeroInstance * army, const CGHeroInstance * source) { //TODO: merge with pickBestCreatures //if (ai->primaryHero().h == source) if(army->tempOwner != source->tempOwner) { logAi->error("Why are we even considering exchange between heroes from different players?"); return false; } const CArmedInstance * armies[] = {army, source}; //we calculate total strength for each creature type available in armies std::map creToPower; for(auto armyPtr : armies) { for(auto & i : armyPtr->Slots()) { //TODO: allow splitting stacks? creToPower[i.second->type] += i.second->getPower(); } } //TODO - consider more than just power (ie morale penalty, hero specialty in certain stacks, etc) int armySize = creToPower.size(); armySize = std::min((source->needsLastStack() ? armySize - 1 : armySize), GameConstants::ARMY_SIZE); //can't move away last stack std::vector bestArmy; //types that'll be in final dst army for(int i = 0; i < armySize; i++) //pick the creatures from which we can get most power, as many as dest can fit { typedef const std::pair & CrePowerPair; auto creIt = boost::max_element(creToPower, [](CrePowerPair lhs, CrePowerPair rhs) { return lhs.second < rhs.second; }); bestArmy.push_back(creIt->first); creToPower.erase(creIt); if(creToPower.empty()) break; } //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs for(int i = 0; i < bestArmy.size(); i++) //i-th strongest creature type will go to i-th slot { for(auto armyPtr : armies) { for(int j = 0; j < GameConstants::ARMY_SIZE; j++) { if(armyPtr->getCreature(SlotID(j)) == bestArmy[i] && armyPtr != army) //it's a searched creature not in dst ARMY { //FIXME: line below is useless when simulating exchange between two non-singular armies if(!(armyPtr->needsLastStack() && armyPtr->stacksCount() == 1)) //can't take away last creature return true; //at least one exchange will be performed else return false; //no further exchange possible } } } } return false; } void VCAI::pickBestCreatures(const CArmedInstance * army, const CArmedInstance * source) { //TODO - what if source is a hero (the last stack problem) -> it'd good to create a single stack of weakest cre const CArmedInstance * armies[] = {army, source}; //we calculate total strength for each creature type available in armies std::map creToPower; for(auto armyPtr : armies) { for(auto & i : armyPtr->Slots()) { //TODO: allow splitting stacks? creToPower[i.second->type] += i.second->getPower(); } } //TODO - consider more than just power (ie morale penalty, hero specialty in certain stacks, etc) int armySize = creToPower.size(); armySize = std::min((source->needsLastStack() ? armySize - 1 : armySize), GameConstants::ARMY_SIZE); //can't move away last stack std::vector bestArmy; //types that'll be in final dst army for(int i = 0; i < armySize; i++) //pick the creatures from which we can get most power, as many as dest can fit { typedef const std::pair & CrePowerPair; auto creIt = boost::max_element(creToPower, [](CrePowerPair lhs, CrePowerPair rhs) { return lhs.second < rhs.second; }); bestArmy.push_back(creIt->first); creToPower.erase(creIt); if(creToPower.empty()) break; } //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs for(int i = 0; i < bestArmy.size(); i++) //i-th strongest creature type will go to i-th slot { for(auto armyPtr : armies) { for(int j = 0; j < GameConstants::ARMY_SIZE; j++) { if(armyPtr->getCreature(SlotID(j)) == bestArmy[i] && (i != j || armyPtr != army)) //it's a searched creature not in dst SLOT { if(!(armyPtr->needsLastStack() && armyPtr->stacksCount() == 1)) //can't take away last creature cb->mergeOrSwapStacks(armyPtr, army, SlotID(j), SlotID(i)); } } } } //TODO - having now strongest possible army, we may want to think about arranging stacks auto hero = dynamic_cast(army); if(hero) checkHeroArmy(hero); } void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other) { auto equipBest = [](const CGHeroInstance * h, const CGHeroInstance * otherh, bool giveStuffToFirstHero) -> void { bool changeMade = false; do { changeMade = false; //we collect gear always in same order std::vector allArtifacts; if(giveStuffToFirstHero) { for(auto p : h->artifactsWorn) { if(p.second.artifact) allArtifacts.push_back(ArtifactLocation(h, p.first)); } } for(auto slot : h->artifactsInBackpack) allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact))); if(otherh) { for(auto p : otherh->artifactsWorn) { if(p.second.artifact) allArtifacts.push_back(ArtifactLocation(otherh, p.first)); } for(auto slot : otherh->artifactsInBackpack) allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact))); } //we give stuff to one hero or another, depending on giveStuffToFirstHero const CGHeroInstance * target = nullptr; if(giveStuffToFirstHero || !otherh) target = h; else target = otherh; for(auto location : allArtifacts) { if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST) continue; //don't reequip artifact we already wear if(location.slot == ArtifactPosition::MACH4) // don't attempt to move catapult continue; auto s = location.getSlot(); if(!s || s->locked) //we can't move locks continue; auto artifact = s->artifact; if(!artifact) continue; //FIXME: why are the above possible to be null? bool emptySlotFound = false; for(auto slot : artifact->artType->possibleSlots.at(target->bearerType())) { ArtifactLocation destLocation(target, slot); if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move { cb->swapArtifacts(location, destLocation); //just put into empty slot emptySlotFound = true; changeMade = true; break; } } if(!emptySlotFound) //try to put that atifact in already occupied slot { for(auto slot : artifact->artType->possibleSlots.at(target->bearerType())) { auto otherSlot = target->getSlot(slot); if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one { ArtifactLocation destLocation(target, slot); //if that artifact is better than what we have, pick it if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move { cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact))); changeMade = true; break; } } } } if(changeMade) break; //start evaluating artifacts from scratch } } while(changeMade); }; equipBest(h, other, true); if(other) equipBest(h, other, false); } void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter) { //now used only for visited dwellings / towns, not BuyArmy goal for(int i = 0; i < d->creatures.size(); i++) { if(!d->creatures[i].second.size()) continue; int count = d->creatures[i].first; CreatureID creID = d->creatures[i].second.back(); vstd::amin(count, ah->freeResources() / VLC->creh->creatures[creID]->cost); if(count > 0) cb->recruitCreatures(d, recruiter, creID, count, i); } } bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, boost::optional movementCostLimit) { int3 op = obj->visitablePos(); auto paths = ah->getPathsToTile(h, op); for(const auto & path : paths) { if(movementCostLimit && movementCostLimit.get() < path.movementCost()) return false; if(isGoodForVisit(obj, h, path)) return true; } return false; } bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const { const int3 pos = obj->visitablePos(); const int3 targetPos = path.firstTileToGet(); if (!targetPos.valid()) return false; if (!isTileNotReserved(h.get(), targetPos)) return false; if (obj->wasVisited(playerID)) return false; if (cb->getPlayerRelations(playerID, obj->tempOwner) != PlayerRelations::ENEMIES && !isWeeklyRevisitable(obj)) return false; // Otherwise we flag or get weekly resources / creatures if (!isSafeToVisit(h, pos)) return false; if (!shouldVisit(h, obj)) return false; if (vstd::contains(alreadyVisited, obj)) return false; if (vstd::contains(reservedObjs, obj)) return false; // TODO: looks extra if we already have AIPath //if (!isAccessibleForHero(targetPos, h)) // return false; const CGObjectInstance * topObj = cb->getVisitableObjs(obj->visitablePos()).back(); //it may be hero visiting this obj //we don't try visiting object on which allied or owned hero stands // -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited if (topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES) return false; else return true; //all of the following is met } bool VCAI::isTileNotReserved(const CGHeroInstance * h, int3 t) const { if(t.valid()) { auto obj = cb->getTopObj(t); if(obj && vstd::contains(ai->reservedObjs, obj) && vstd::contains(reservedHeroesMap, h) && !vstd::contains(reservedHeroesMap.at(h), obj)) return false; //do not capture object reserved by another hero else return true; } else { return false; } } bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const { //TODO: make gathering gold, building tavern or conquering town (?) possible subgoals if(!t) t = findTownWithTavern(); if(!t) return false; if(cb->getResourceAmount(Res::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager return false; if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES) return false; if(!cb->getAvailableHeroes(t).size()) return false; return true; } void VCAI::wander(HeroPtr h) { auto visitTownIfAny = [this](HeroPtr h) -> bool { if (h->visitedTown) { townVisitsThisWeek[h].insert(h->visitedTown); buildArmyIn(h->visitedTown); return true; } return false; }; //unclaim objects that are now dangerous for us auto reservedObjsSetCopy = reservedHeroesMap[h]; for(auto obj : reservedObjsSetCopy) { if(!isSafeToVisit(h, obj->visitablePos())) unreserveObject(h, obj); } TimeCheck tc("looking for wander destination"); while(h->movement) { validateVisitableObjs(); std::vector dests; //also visit our reserved objects - but they are not prioritized to avoid running back and forth vstd::copy_if(reservedHeroesMap[h], std::back_inserter(dests), [&](ObjectIdRef obj) -> bool { return ah->getPathsToTile(h, obj->visitablePos()).size(); }); int pass = 0; std::vector> distanceLimits = { 1.0, 2.0, boost::none }; while(!dests.size() && pass < distanceLimits.size()) { auto & distanceLimit = distanceLimits[pass]; logAi->debug("Looking for wander destination pass=%i, cost limit=%f", pass, distanceLimit.get_value_or(-1.0)); vstd::copy_if(visitableObjs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool { return isGoodForVisit(obj, h, distanceLimit); }); pass++; } if(!dests.size()) { logAi->debug("Looking for town destination"); if(cb->getVisitableObjs(h->visitablePos()).size() > 1) moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate auto compareReinforcements = [h](const CGTownInstance * lhs, const CGTownInstance * rhs) -> bool { const CGHeroInstance * hptr = h.get(); auto r1 = howManyReinforcementsCanGet(hptr, lhs), r2 = howManyReinforcementsCanGet(hptr, rhs); if (r1 != r2) return r1 < r2; else return howManyReinforcementsCanBuy(hptr, lhs) < howManyReinforcementsCanBuy(hptr, rhs); }; std::vector townsReachable; std::vector townsNotReachable; for(const CGTownInstance * t : cb->getTownsInfo()) { if(!t->visitingHero && !vstd::contains(townVisitsThisWeek[h], t)) { if(isAccessibleForHero(t->visitablePos(), h)) townsReachable.push_back(t); else townsNotReachable.push_back(t); } } if(townsReachable.size()) //travel to town with largest garrison, or empty - better than nothing { dests.push_back(*boost::max_element(townsReachable, compareReinforcements)); } else if(townsNotReachable.size()) { //TODO pick the truly best const CGTownInstance * t = *boost::max_element(townsNotReachable, compareReinforcements); logAi->debug("%s can't reach any town, we'll try to make our way to %s at %s", h->name, t->name, t->visitablePos().toString()); int3 pos1 = h->pos; striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); //TODO: drop "strive", add to mainLoop //if out hero is stuck, we may need to request another hero to clear the way we see if(pos1 == h->pos && h == primaryHero()) //hero can't move { if(canRecruitAnyHero(t)) recruitHero(t); } break; } else if(cb->getResourceAmount(Res::GOLD) >= GameConstants::HERO_GOLD_COST) { std::vector towns = cb->getTownsInfo(); vstd::erase_if(towns, [](const CGTownInstance * t) -> bool { for(const CGHeroInstance * h : cb->getHeroesInfo()) { if(!t->getArmyStrength() || howManyReinforcementsCanGet(h, t)) return true; } return false; }); if (towns.size()) { recruitHero(*boost::max_element(towns, compareArmyStrength)); } break; } else { logAi->debug("Nowhere more to go..."); break; } } //end of objs empty if(dests.size()) //performance improvement { Goals::TGoalVec targetObjectGoals; for(auto destination : dests) { vstd::concatenate(targetObjectGoals, ah->howToVisitObj(h, destination, false)); } if(targetObjectGoals.size()) { auto bestObjectGoal = fh->chooseSolution(targetObjectGoals); //wander should not cause heroes to be reserved - they are always considered free if(bestObjectGoal->goalType == Goals::VISIT_OBJ) { auto chosenObject = cb->getObjInstance(ObjectInstanceID(bestObjectGoal->objid)); if(chosenObject != nullptr) logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->pos.toString()); } else logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name()); try { decomposeGoal(bestObjectGoal)->accept(this); } catch(const goalFulfilledException & e) { if(e.goal->goalType == Goals::EGoals::VISIT_TILE || e.goal->goalType == Goals::EGoals::VISIT_OBJ) continue; throw e; } } else { logAi->debug("Nowhere more to go..."); break; } visitTownIfAny(h); } } visitTownIfAny(h); //in case hero is just sitting in town } void VCAI::setGoal(HeroPtr h, Goals::TSubgoal goal) { if(goal->invalid()) { vstd::erase_if_present(lockedHeroes, h); } else { lockedHeroes[h] = goal; goal->setisElementar(false); //Force always evaluate goals before realizing } } void VCAI::evaluateGoal(HeroPtr h) { if(vstd::contains(lockedHeroes, h)) fh->setPriority(lockedHeroes[h]); } void VCAI::completeGoal(Goals::TSubgoal goal) { if (goal->goalType == Goals::WIN) //we can never complete this goal - unless we already won return; logAi->debug("Completing goal: %s", goal->name()); //notify Managers ah->notifyGoalCompleted(goal); //notify mainLoop() goalsToRemove.push_back(goal); //will be removed from mainLoop() goals for (auto basicGoal : basicGoals) //we could luckily fulfill any of our goals { if (basicGoal->fulfillsMe(goal)) goalsToRemove.push_back(basicGoal); } //unreserve heroes if(const CGHeroInstance * h = goal->hero.get(true)) { auto it = lockedHeroes.find(h); if(it != lockedHeroes.end()) { if(it->second == goal || it->second->fulfillsMe(goal)) //FIXME this is overspecified, fulfillsMe shoudl be complete { logAi->debug(goal->completeMessage()); lockedHeroes.erase(it); //goal fulfilled, free hero } } } else //complete goal for all heroes maybe? { vstd::erase_if(lockedHeroes, [goal](std::pair p) { if(p.second == goal || p.second->fulfillsMe(goal)) //we could have fulfilled goals of other heroes by chance { logAi->debug(p.second->completeMessage()); return true; } return false; }); } } void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) { NET_EVENT_HANDLER; assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); status.setBattle(ONGOING_BATTLE); const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->name : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); } void VCAI::battleEnd(const BattleResult * br) { NET_EVENT_HANDLER; assert(status.getBattle() == ONGOING_BATTLE); status.setBattle(ENDING_BATTLE); bool won = br->winner == myCb->battleGetMySide(); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); CAdventureAI::battleEnd(br); } void VCAI::waitTillFree() { auto unlock = vstd::makeUnlockSharedGuard(CGameState::mutex); status.waitTillFree(); } void VCAI::markObjectVisited(const CGObjectInstance * obj) { if(!obj) return; if(dynamic_cast(obj)) //we may want to visit it with another hero return; if(dynamic_cast(obj)) //or another time return; if(obj->ID == Obj::MONSTER) return; alreadyVisited.insert(obj); } void VCAI::reserveObject(HeroPtr h, const CGObjectInstance * obj) { reservedObjs.insert(obj); reservedHeroesMap[h].insert(obj); logAi->debug("reserved object id=%d; address=%p; name=%s", obj->id, obj, obj->getObjectName()); } void VCAI::unreserveObject(HeroPtr h, const CGObjectInstance * obj) { vstd::erase_if_present(reservedObjs, obj); //unreserve objects vstd::erase_if_present(reservedHeroesMap[h], obj); } void VCAI::markHeroUnableToExplore(HeroPtr h) { heroesUnableToExplore.insert(h); } void VCAI::markHeroAbleToExplore(HeroPtr h) { vstd::erase_if_present(heroesUnableToExplore, h); } bool VCAI::isAbleToExplore(HeroPtr h) { return !vstd::contains(heroesUnableToExplore, h); } void VCAI::clearPathsInfo() { heroesUnableToExplore.clear(); ah->resetPaths(); } void VCAI::validateVisitableObjs() { std::string errorMsg; auto shouldBeErased = [&](const CGObjectInstance * obj) -> bool { if(obj) return !cb->getObj(obj->id, false); // no verbose output needed as we check object visibility else return true; }; //errorMsg is captured by ref so lambda will take the new text errorMsg = " shouldn't be on the visitable objects list!"; vstd::erase_if(visitableObjs, shouldBeErased); //FIXME: how comes our own heroes become inaccessible? vstd::erase_if(reservedHeroesMap, [](std::pair> hp) -> bool { return !hp.first.get(true); }); for(auto & p : reservedHeroesMap) { errorMsg = " shouldn't be on list for hero " + p.first->name + "!"; vstd::erase_if(p.second, shouldBeErased); } errorMsg = " shouldn't be on the reserved objs list!"; vstd::erase_if(reservedObjs, shouldBeErased); //TODO overkill, hidden object should not be removed. However, we can't know if hidden object is erased from game. errorMsg = " shouldn't be on the already visited objs list!"; vstd::erase_if(alreadyVisited, shouldBeErased); } void VCAI::retrieveVisitableObjs(std::vector & out, bool includeOwned) const { foreach_tile_pos([&](const int3 & pos) { for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) { if(includeOwned || obj->tempOwner != playerID) out.push_back(obj); } }); } void VCAI::retrieveVisitableObjs() { foreach_tile_pos([&](const int3 & pos) { for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) { if(obj->tempOwner != playerID) addVisitableObj(obj); } }); } std::vector VCAI::getFlaggedObjects() const { std::vector ret; for(const CGObjectInstance * obj : visitableObjs) { if(obj->tempOwner == playerID) ret.push_back(obj); } return ret; } void VCAI::addVisitableObj(const CGObjectInstance * obj) { if(obj->ID == Obj::EVENT) return; visitableObjs.insert(obj); // All teleport objects seen automatically assigned to appropriate channels auto teleportObj = dynamic_cast(obj); if(teleportObj) CGTeleport::addToChannel(knownTeleportChannels, teleportObj); } const CGObjectInstance * VCAI::lookForArt(int aid) const { for(const CGObjectInstance * obj : ai->visitableObjs) { if(obj->ID == Obj::ARTIFACT && obj->subID == aid) return obj; } return nullptr; //TODO what if more than one artifact is available? return them all or some slection criteria } bool VCAI::isAccessible(const int3 & pos) const { //TODO precalculate for speed for(const CGHeroInstance * h : cb->getHeroesInfo()) { if(isAccessibleForHero(pos, h)) return true; } return false; } HeroPtr VCAI::getHeroWithGrail() const { for(const CGHeroInstance * h : cb->getHeroesInfo()) { if(h->hasArt(2)) //grail return h; } return nullptr; } const CGObjectInstance * VCAI::getUnvisitedObj(const std::function & predicate) { //TODO smarter definition of unvisited for(const CGObjectInstance * obj : visitableObjs) { if(predicate(obj) && !vstd::contains(alreadyVisited, obj)) return obj; } return nullptr; } bool VCAI::isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies) const { // Don't visit tile occupied by allied hero if(!includeAllies) { for(auto obj : cb->getVisitableObjs(pos)) { if(obj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, obj->tempOwner) != PlayerRelations::ENEMIES) { if(obj != h.get()) return false; } } } return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable(); } bool VCAI::moveHeroToTile(int3 dst, HeroPtr h) { //TODO: consider if blockVisit objects change something in our checks: AIUtility::isBlockVisitObj() auto afterMovementCheck = [&]() -> void { waitTillFree(); //movement may cause battle or blocking dialog if(!h) { lostHero(h); teleportChannelProbingList.clear(); if(status.channelProbing()) // if hero lost during channel probing we need to switch this mode off status.setChannelProbing(false); throw cannotFulfillGoalException("Hero was lost!"); } }; logAi->debug("Moving hero %s to tile %s", h->name, dst.toString()); int3 startHpos = h->visitablePos(); bool ret = false; if(startHpos == dst) { //FIXME: this assertion fails also if AI moves onto defeated guarded object assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true)); afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly? // not sure if AI can currently reconsider to attack bank while staying on it. Check issue 2084 on mantis for more information. ret = true; } else { CGPath path; cb->getPathsInfo(h.get())->getPath(path, dst); if(path.nodes.empty()) { logAi->error("Hero %s cannot reach %s.", h->name, dst.toString()); throw goalFulfilledException(sptr(Goals::VisitTile(dst).sethero(h))); } int i = path.nodes.size() - 1; auto getObj = [&](int3 coord, bool ignoreHero) { auto tile = cb->getTile(coord, false); assert(tile); return tile->topVisitableObj(ignoreHero); //return cb->getTile(coord,false)->topVisitableObj(ignoreHero); }; auto isTeleportAction = [&](CGPathNode::ENodeAction action) -> bool { if(action != CGPathNode::TELEPORT_NORMAL && action != CGPathNode::TELEPORT_BLOCKING_VISIT) { if(action != CGPathNode::TELEPORT_BATTLE) { return false; } } return true; }; auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * { if(CGTeleport::isConnected(currentObject, nextObjectTop)) return nextObjectTop; if(nextObjectTop && nextObjectTop->ID == Obj::HERO) { if(CGTeleport::isConnected(currentObject, nextObject)) return nextObject; } return nullptr; }; auto doMovement = [&](int3 dst, bool transit) { cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true), transit); }; auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos) { destinationTeleport = exitId; if(exitPos.valid()) destinationTeleportPos = CGHeroInstance::convertPosition(exitPos, true); cb->moveHero(*h, h->pos); destinationTeleport = ObjectInstanceID(); destinationTeleportPos = int3(-1); afterMovementCheck(); }; auto doChannelProbing = [&]() -> void { auto currentPos = CGHeroInstance::convertPosition(h->pos, false); auto currentExit = getObj(currentPos, true)->id; status.setChannelProbing(true); for(auto exit : teleportChannelProbingList) doTeleportMovement(exit, int3(-1)); teleportChannelProbingList.clear(); status.setChannelProbing(false); doTeleportMovement(currentExit, currentPos); }; for(; i > 0; i--) { int3 currentCoord = path.nodes[i].coord; int3 nextCoord = path.nodes[i - 1].coord; auto currentObject = getObj(currentCoord, currentCoord == CGHeroInstance::convertPosition(h->pos, false)); auto nextObjectTop = getObj(nextCoord, false); auto nextObject = getObj(nextCoord, true); auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject); if(isTeleportAction(path.nodes[i - 1].action) && destTeleportObj != nullptr) { //we use special login if hero standing on teleporter it's mean we need doTeleportMovement(destTeleportObj->id, nextCoord); if(teleportChannelProbingList.size()) doChannelProbing(); markObjectVisited(destTeleportObj); //FIXME: Monoliths are not correctly visited continue; } //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) if(path.nodes[i - 1].turns) { //blockedHeroes.insert(h); //to avoid attempts of moving heroes with very little MPs break; } int3 endpos = path.nodes[i - 1].coord; if(endpos == h->visitablePos()) continue; bool isConnected = false; bool isNextObjectTeleport = false; // Check there is node after next one; otherwise transit is pointless if(i - 2 >= 0) { isConnected = CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i - 2].coord, false)); isNextObjectTeleport = CGTeleport::isTeleport(nextObjectTop); } if(isConnected || isNextObjectTeleport) { // Hero should be able to go through object if it's allow transit doMovement(endpos, true); } else if(path.nodes[i - 1].layer == EPathfindingLayer::AIR) { doMovement(endpos, true); } else { doMovement(endpos, false); } afterMovementCheck(); if(teleportChannelProbingList.size()) doChannelProbing(); } if(path.nodes[0].action == CGPathNode::BLOCKING_VISIT) { ret = h && i == 0; // when we take resource we do not reach its position. We even might not move } } if(h) { if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting { if(visitedObject != *h) performObjectInteraction(visitedObject, h); } } if(h) //we could have lost hero after last move { completeGoal(sptr(Goals::VisitTile(dst).sethero(h))); //we stepped on some tile, anyway completeGoal(sptr(Goals::ClearWayTo(dst).sethero(h))); ret = ret || (dst == h->visitablePos()); if(!ret) //reserve object we are heading towards { auto obj = vstd::frontOrNull(cb->getVisitableObjs(dst)); if(obj && obj != *h) reserveObject(h, obj); } if(startHpos == h->visitablePos() && !ret) //we didn't move and didn't reach the target { vstd::erase_if_present(lockedHeroes, h); //hero seemingly is confused or has only 95mp which is not enough to move invalidPathHeroes.insert(h); throw cannotFulfillGoalException("Invalid path found!"); } evaluateGoal(h); //new hero position means new game situation logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->name, startHpos.toString(), h->visitablePos().toString(), ret); } return ret; } void VCAI::buildStructure(const CGTownInstance * t, BuildingID building) { auto name = t->town->buildings.at(building)->Name(); logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->name, t->pos.toString()); cb->buildBuilding(t, building); //just do this; } void VCAI::tryRealize(Goals::Explore & g) { throw cannotFulfillGoalException("EXPLORE is not an elementar goal!"); } void VCAI::tryRealize(Goals::RecruitHero & g) { if(const CGTownInstance * t = findTownWithTavern()) { recruitHero(t, true); //TODO try to free way to blocked town //TODO: adventure map tavern or prison? } else { throw cannotFulfillGoalException("No town to recruit hero!"); } } void VCAI::tryRealize(Goals::VisitTile & g) { if(!g.hero->movement) throw cannotFulfillGoalException("Cannot visit tile: hero is out of MPs!"); if(g.tile == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) { logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->name, g.tile.toString()); throw goalFulfilledException(sptr(g)); } if(ai->moveHeroToTile(g.tile, g.hero.get())) { throw goalFulfilledException(sptr(g)); } } void VCAI::tryRealize(Goals::VisitObj & g) { auto position = g.tile; if(!g.hero->movement) throw cannotFulfillGoalException("Cannot visit object: hero is out of MPs!"); if(position == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) { logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->name, g.tile.toString()); throw goalFulfilledException(sptr(g)); } if(ai->moveHeroToTile(position, g.hero.get())) { throw goalFulfilledException(sptr(g)); } } void VCAI::tryRealize(Goals::VisitHero & g) { if(!g.hero->movement) throw cannotFulfillGoalException("Cannot visit target hero: hero is out of MPs!"); const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid)); if(obj) { if(ai->moveHeroToTile(obj->visitablePos(), g.hero.get())) { throw goalFulfilledException(sptr(g)); } } else { throw cannotFulfillGoalException("Cannot visit hero: object not found!"); } } void VCAI::tryRealize(Goals::BuildThis & g) { auto b = BuildingID(g.bid); auto t = g.town; if (t) { if (cb->canBuildStructure(t, b) == EBuildingState::ALLOWED) { logAi->debug("Player %d will build %s in town of %s at %s", playerID, t->town->buildings.at(b)->Name(), t->name, t->pos.toString()); cb->buildBuilding(t, b); throw goalFulfilledException(sptr(g)); } } throw cannotFulfillGoalException("Cannot build a given structure!"); } void VCAI::tryRealize(Goals::DigAtTile & g) { assert(g.hero->visitablePos() == g.tile); //surely we want to crash here? if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG) { cb->dig(g.hero.get()); completeGoal(sptr(g)); // finished digging } else { ai->lockedHeroes[g.hero] = sptr(g); //hero who tries to dig shouldn't do anything else throw cannotFulfillGoalException("A hero can't dig!\n"); } } void VCAI::tryRealize(Goals::Trade & g) //trade { if(ah->freeResources()[g.resID] >= g.value) //goal is already fulfilled. Why we need this check, anyway? throw goalFulfilledException(sptr(g)); int accquiredResources = 0; if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) { if(const IMarket * m = IMarket::castFrom(obj, false)) { auto freeRes = ah->freeResources(); //trade only resources which are not reserved for(auto it = Res::ResourceSet::nziterator(freeRes); it.valid(); it++) { auto res = it->resType; if(res == g.resID) //sell any other resource continue; int toGive, toGet; m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); toGive = toGive * (it->resVal / toGive); //round down //TODO trade only as much as needed if (toGive) //don't try to sell 0 resources { cb->trade(obj, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive); accquiredResources = toGet * (it->resVal / toGive); logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName()); } if (ah->freeResources()[g.resID] >= g.value) throw goalFulfilledException(sptr(g)); //we traded all we needed } throw cannotFulfillGoalException("I cannot get needed resources by trade!"); } else { throw cannotFulfillGoalException("I don't know how to use this object to raise resources!"); } } else { throw cannotFulfillGoalException("No object that could be used to raise resources!"); } } void VCAI::tryRealize(Goals::BuyArmy & g) { auto t = g.town; ui64 valueBought = 0; //buy the stacks with largest AI value makePossibleUpgrades(t); while (valueBought < g.value) { auto res = ah->allResources(); std::vector creaturesInDwellings; for (int i = 0; i < t->creatures.size(); i++) { auto ci = infoFromDC(t->creatures[i]); if(!ci.count || ci.creID == -1 || (g.objid != -1 && ci.creID != g.objid) || t->getUpperArmy()->getSlotFor(ci.creID) == SlotID()) continue; vstd::amin(ci.count, res / ci.cre->cost); //max count we can afford if(!ci.count) continue; ci.level = i; //this is important for Dungeon Summoning Portal creaturesInDwellings.push_back(ci); } if (creaturesInDwellings.empty()) throw cannotFulfillGoalException("Can't buy any more creatures!"); creInfo ci = *boost::max_element(creaturesInDwellings, [](const creInfo & lhs, const creInfo & rhs) { //max value of creatures we can buy with our res int value1 = lhs.cre->AIValue * lhs.count, value2 = rhs.cre->AIValue * rhs.count; return value1 < value2; }); cb->recruitCreatures(t, t->getUpperArmy(), ci.creID, ci.count, ci.level); valueBought += ci.count * ci.cre->AIValue; } throw goalFulfilledException(sptr(g)); //we bought as many creatures as we wanted } void VCAI::tryRealize(Goals::Invalid & g) { throw cannotFulfillGoalException("I don't know how to fulfill this!"); } void VCAI::tryRealize(Goals::AbstractGoal & g) { logAi->debug("Attempting realizing goal with code %s", g.name()); throw cannotFulfillGoalException("Unknown type of goal !"); } const CGTownInstance * VCAI::findTownWithTavern() const { for(const CGTownInstance * t : cb->getTownsInfo()) if(t->hasBuilt(BuildingID::TAVERN) && !t->visitingHero) return t; return nullptr; } Goals::TSubgoal VCAI::getGoal(HeroPtr h) const { auto it = lockedHeroes.find(h); if(it != lockedHeroes.end()) return it->second; else return sptr(Goals::Invalid()); } std::vector VCAI::getUnblockedHeroes() const { std::vector ret; for(auto h : cb->getHeroesInfo()) { //&& !vstd::contains(lockedHeroes, h) //at this point we assume heroes exhausted their locked goals if(canAct(h)) ret.push_back(h); } return ret; } bool VCAI::canAct(HeroPtr h) const { auto mission = lockedHeroes.find(h); if(mission != lockedHeroes.end()) { //FIXME: I'm afraid there can be other conditions when heroes can act but not move :? if(mission->second->goalType == Goals::DIG_AT_TILE && !mission->second->isElementar) return false; } return h->movement; } HeroPtr VCAI::primaryHero() const { auto hs = cb->getHeroesInfo(); if (hs.empty()) return nullptr; else return *boost::max_element(hs, compareHeroStrength); } void VCAI::endTurn() { logAi->info("Player %d (%s) ends turn", playerID, playerID.getStr()); if(!status.haveTurn()) { logAi->error("Not having turn at the end of turn???"); } logAi->debug("Resources at the end of turn: %s", cb->getResourceAmount().toString()); do { cb->endTurn(); } while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over logGlobal->info("Player %d (%s) ended turn", playerID, playerID.getStr()); } void VCAI::striveToGoal(Goals::TSubgoal basicGoal) { //TODO: this function is deprecated and should be dropped altogether auto goalToDecompose = basicGoal; Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); int maxAbstractGoals = 10; while (!elementarGoal->isElementar && maxAbstractGoals) { try { elementarGoal = decomposeGoal(goalToDecompose); } catch (goalFulfilledException & e) { //it is impossible to continue some goals (like exploration, for example) completeGoal(e.goal); //put in goalsToRemove logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); return; } catch (std::exception & e) { goalsToRemove.push_back(basicGoal); logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); return; } if (elementarGoal->isAbstract) //we can decompose it further { goalsToAdd.push_back(elementarGoal); //decompose further now - this is necesssary if we can't add over 10 goals in the pool goalToDecompose = elementarGoal; //there is a risk of infinite abstract goal loop, though it indicates failed logic maxAbstractGoals--; } else if (elementarGoal->isElementar) //should be { logAi->debug("Found elementar goal %s", elementarGoal->name()); ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? break; } else //should never be here throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); } //realize best goal if (!elementarGoal->invalid()) { logAi->debug("Trying to realize %s (value %2.3f)", elementarGoal->name(), elementarGoal->priority); try { boost::this_thread::interruption_point(); elementarGoal->accept(this); //visitor pattern boost::this_thread::interruption_point(); } catch (boost::thread_interrupted & e) { logAi->debug("Player %d: Making turn thread received an interruption!", playerID); throw; //rethrow, we want to truly end this thread } catch (goalFulfilledException & e) { //the sub-goal was completed successfully completeGoal(e.goal); //local goal was also completed completeGoal(elementarGoal); } catch (std::exception & e) { logAi->debug("Failed to realize subgoal of type %s, I will stop.", elementarGoal->name()); logAi->debug("The error message was: %s", e.what()); //erase base goal if we failed to execute decomposed goal for (auto basicGoalToRemove : ultimateGoalsFromBasic[elementarGoal]) goalsToRemove.push_back(basicGoalToRemove); } } } Goals::TSubgoal VCAI::decomposeGoal(Goals::TSubgoal ultimateGoal) { if(ultimateGoal->isElementar) { logAi->warn("Trying to decompose elementar goal %s", ultimateGoal->name()); return ultimateGoal; } const int searchDepth = 30; Goals::TSubgoal goal = ultimateGoal; logAi->debug("Decomposing goal %s", ultimateGoal->name()); int maxGoals = searchDepth; //preventing deadlock for mutually dependent goals while (maxGoals) { boost::this_thread::interruption_point(); goal = goal->whatToDoToAchieve(); //may throw if decomposition fails --maxGoals; if (goal == ultimateGoal) //compare objects by value if (goal->isElementar == ultimateGoal->isElementar) throw cannotFulfillGoalException((boost::format("Goal dependency loop detected for %s!") % ultimateGoal->name()).str()); if (goal->isAbstract || goal->isElementar) return goal; else logAi->debug("Considering: %s", goal->name()); } throw cannotFulfillGoalException("Too many subgoals, don't know what to do"); } void VCAI::performTypicalActions() { for(auto h : getUnblockedHeroes()) { if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn continue; logAi->debug("Hero %s started wandering, MP=%d", h->name.c_str(), h->movement); makePossibleUpgrades(*h); pickBestArtifacts(*h); try { wander(h); } catch(std::exception & e) { logAi->debug("Cannot use this hero anymore, received exception: %s", e.what()); continue; } } } void VCAI::buildArmyIn(const CGTownInstance * t) { makePossibleUpgrades(t->visitingHero); makePossibleUpgrades(t); recruitCreatures(t, t->getUpperArmy()); moveCreaturesToHero(t); } void VCAI::checkHeroArmy(HeroPtr h) { auto it = lockedHeroes.find(h); if(it != lockedHeroes.end()) { if(it->second->goalType == Goals::GATHER_ARMY && it->second->value <= h->getArmyStrength()) completeGoal(sptr(Goals::GatherArmy(it->second->value).sethero(h))); } } void VCAI::recruitHero(const CGTownInstance * t, bool throwing) { logAi->debug("Trying to recruit a hero in %s at %s", t->name, t->visitablePos().toString()); auto heroes = cb->getAvailableHeroes(t); if(heroes.size()) { auto hero = heroes[0]; if(heroes.size() >= 2) //makes sense to recruit two heroes with starting amries in first week { if(heroes[1]->getTotalStrength() > hero->getTotalStrength()) hero = heroes[1]; } cb->recruitHero(t, hero); throw goalFulfilledException(sptr(Goals::RecruitHero().settown(t))); } else if(throwing) { throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); } } void VCAI::finish() { if(makingTurn) { makingTurn->interrupt(); makingTurn->join(); makingTurn.reset(); } } void VCAI::requestActionASAP(std::function whatToDo) { boost::thread newThread([this, whatToDo]() { setThreadName("VCAI::requestActionASAP::whatToDo"); SET_GLOBAL_STATE(this); boost::shared_lock gsLock(CGameState::mutex); whatToDo(); }); } void VCAI::lostHero(HeroPtr h) { logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name); vstd::erase_if_present(lockedHeroes, h); for(auto obj : reservedHeroesMap[h]) { vstd::erase_if_present(reservedObjs, obj); //unreserve all objects for that hero } vstd::erase_if_present(reservedHeroesMap, h); vstd::erase_if_present(visitedHeroes, h); for (auto heroVec : visitedHeroes) { vstd::erase_if_present(heroVec.second, h); } //remove goals with removed hero assigned from main loop vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool { if(x.first->hero == h) return true; else return false; }); auto removedHeroGoalPredicate = [&](const Goals::TSubgoal & x) ->bool { if(x->hero == h) return true; else return false; }; vstd::erase_if(basicGoals, removedHeroGoalPredicate); vstd::erase_if(goalsToAdd, removedHeroGoalPredicate); vstd::erase_if(goalsToRemove, removedHeroGoalPredicate); for(auto goal : ultimateGoalsFromBasic) vstd::erase_if(goal.second, removedHeroGoalPredicate); } void VCAI::answerQuery(QueryID queryID, int selection) { logAi->debug("I'll answer the query %d giving the choice %d", queryID, selection); if(queryID != QueryID(-1)) { cb->selectionMade(selection, queryID); } else { logAi->debug("Since the query ID is %d, the answer won't be sent. This is not a real query!", queryID); //do nothing } } void VCAI::requestSent(const CPackForServer * pack, int requestID) { //BNLOG("I have sent request of type %s", typeid(*pack).name()); if(auto reply = dynamic_cast(pack)) { status.attemptedAnsweringQuery(reply->qid, requestID); } } std::string VCAI::getBattleAIName() const { if(settings["server"]["enemyAI"].getType() == JsonNode::JsonType::DATA_STRING) return settings["server"]["enemyAI"].String(); else return "BattleAI"; } void VCAI::validateObject(const CGObjectInstance * obj) { validateObject(obj->id); } void VCAI::validateObject(ObjectIdRef obj) { auto matchesId = [&](const CGObjectInstance * hlpObj) -> bool { return hlpObj->id == obj.id; }; if(!obj) { vstd::erase_if(visitableObjs, matchesId); for(auto & p : reservedHeroesMap) vstd::erase_if(p.second, matchesId); vstd::erase_if(reservedObjs, matchesId); } } AIStatus::AIStatus() { battle = NO_BATTLE; havingTurn = false; ongoingHeroMovement = false; ongoingChannelProbing = false; } AIStatus::~AIStatus() { } void AIStatus::setBattle(BattleState BS) { boost::unique_lock lock(mx); LOG_TRACE_PARAMS(logAi, "battle state=%d", (int)BS); battle = BS; cv.notify_all(); } BattleState AIStatus::getBattle() { boost::unique_lock lock(mx); return battle; } void AIStatus::addQuery(QueryID ID, std::string description) { if(ID == QueryID(-1)) { logAi->debug("The \"query\" has an id %d, it'll be ignored as non-query. Description: %s", ID, description); return; } assert(ID.getNum() >= 0); boost::unique_lock lock(mx); assert(!vstd::contains(remainingQueries, ID)); remainingQueries[ID] = description; cv.notify_all(); logAi->debug("Adding query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); } void AIStatus::removeQuery(QueryID ID) { boost::unique_lock lock(mx); assert(vstd::contains(remainingQueries, ID)); std::string description = remainingQueries[ID]; remainingQueries.erase(ID); cv.notify_all(); logAi->debug("Removing query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); } int AIStatus::getQueriesCount() { boost::unique_lock lock(mx); return remainingQueries.size(); } void AIStatus::startedTurn() { boost::unique_lock lock(mx); havingTurn = true; cv.notify_all(); } void AIStatus::madeTurn() { boost::unique_lock lock(mx); havingTurn = false; cv.notify_all(); } void AIStatus::waitTillFree() { boost::unique_lock lock(mx); while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement) cv.timed_wait(lock, boost::posix_time::milliseconds(100)); } bool AIStatus::haveTurn() { boost::unique_lock lock(mx); return havingTurn; } void AIStatus::attemptedAnsweringQuery(QueryID queryID, int answerRequestID) { boost::unique_lock lock(mx); assert(vstd::contains(remainingQueries, queryID)); std::string description = remainingQueries[queryID]; logAi->debug("Attempted answering query %d - %s. Request id=%d. Waiting for results...", queryID, description, answerRequestID); requestToQueryID[answerRequestID] = queryID; } void AIStatus::receivedAnswerConfirmation(int answerRequestID, int result) { assert(vstd::contains(requestToQueryID, answerRequestID)); QueryID query = requestToQueryID[answerRequestID]; assert(vstd::contains(remainingQueries, query)); requestToQueryID.erase(answerRequestID); if(result) { removeQuery(query); } else { logAi->error("Something went really wrong, failed to answer query %d : %s", query.getNum(), remainingQueries[query]); //TODO safely retry } } void AIStatus::heroVisit(const CGObjectInstance * obj, bool started) { boost::unique_lock lock(mx); if(started) { objectsBeingVisited.push_back(obj); } else { // There can be more than one object visited at the time (eg. hero visits Subterranean Gate // causing visit to hero on the other side. // However, we are guaranteed that start/end visit notification maintain stack order. assert(!objectsBeingVisited.empty()); objectsBeingVisited.pop_back(); } cv.notify_all(); } void AIStatus::setMove(bool ongoing) { boost::unique_lock lock(mx); ongoingHeroMovement = ongoing; cv.notify_all(); } void AIStatus::setChannelProbing(bool ongoing) { boost::unique_lock lock(mx); ongoingChannelProbing = ongoing; cv.notify_all(); } bool AIStatus::channelProbing() { return ongoingChannelProbing; } bool isWeeklyRevisitable(const CGObjectInstance * obj) { //TODO: allow polling of remaining creatures in dwelling if(dynamic_cast(obj)) // ensures future compatibility, unlike IDs return true; if(dynamic_cast(obj)) return true; if(dynamic_cast(obj)) //banks tend to respawn often in mods return true; switch(obj->ID) { case Obj::STABLES: case Obj::MAGIC_WELL: case Obj::HILL_FORT: return true; case Obj::BORDER_GATE: case Obj::BORDERGUARD: return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); //FIXME: they could be revisited sooner than in a week } return false; } bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) { switch(obj->ID) { case Obj::TOWN: case Obj::HERO: //never visit our heroes at random return obj->tempOwner != h->tempOwner; //do not visit our towns at random case Obj::BORDER_GATE: { for(auto q : ai->myCb->getMyQuests()) { if(q.obj == obj) { return false; // do not visit guards or gates when wandering } } return true; //we don't have this quest yet } case Obj::BORDERGUARD: //open borderguard if possible return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); case Obj::SEER_HUT: case Obj::QUEST_GUARD: { for(auto q : ai->myCb->getMyQuests()) { if(q.obj == obj) { if(q.quest->checkQuest(h.h)) return true; //we completed the quest else return false; //we can't complete this quest } } return true; //we don't have this quest yet } case Obj::CREATURE_GENERATOR1: { if(obj->tempOwner != h->tempOwner) return true; //flag just in case bool canRecruitCreatures = false; const CGDwelling * d = dynamic_cast(obj); for(auto level : d->creatures) { for(auto c : level.second) { if(h->getSlotFor(CreatureID(c)) != SlotID()) canRecruitCreatures = true; } } return canRecruitCreatures; } case Obj::HILL_FORT: { for(auto slot : h->Slots()) { if(slot.second->type->upgrades.size()) return true; //TODO: check price? } return false; } case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_ONE_WAY_EXIT: case Obj::MONOLITH_TWO_WAY: case Obj::WHIRLPOOL: return false; case Obj::SCHOOL_OF_MAGIC: case Obj::SCHOOL_OF_WAR: { if (ai->ah->freeGold() < 1000) return false; break; } case Obj::LIBRARY_OF_ENLIGHTENMENT: if(h->level < 12) return false; break; case Obj::TREE_OF_KNOWLEDGE: { TResources myRes = ai->ah->freeResources(); if(myRes[Res::GOLD] < 2000 || myRes[Res::GEMS] < 10) return false; break; } case Obj::MAGIC_WELL: return h->mana < h->manaLimit(); case Obj::PRISON: return ai->myCb->getHeroesInfo().size() < VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER; case Obj::TAVERN: { //TODO: make AI actually recruit heroes //TODO: only on request if(ai->myCb->getHeroesInfo().size() >= VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER) return false; else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST) return false; break; } case Obj::BOAT: return false; //Boats are handled by pathfinder case Obj::EYE_OF_MAGI: return false; //this object is useless to visit, but could be visited indefinitely } if(obj->wasVisited(*h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player); return false; return true; } vcmi-0.99+git20190113.f06c8a87/AI/VCAI/VCAI.h000066400000000000000000000410461342332007200170650ustar00rootroot00000000000000/* * VCAI.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "AIUtility.h" #include "Goals/AbstractGoal.h" #include "../../lib/AI_Base.h" #include "../../CCallback.h" #include "../../lib/CThreadHelper.h" #include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" #include "../../lib/CBuildingHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CTownHandler.h" #include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CondSh.h" #include "Pathfinding/AIPathfinder.h" struct QuestInfo; class AIhelper; class AIStatus { boost::mutex mx; boost::condition_variable cv; BattleState battle; std::map remainingQueries; std::map requestToQueryID; //IDs of answer-requests sent to server => query ids (so we can match answer confirmation from server to the query) std::vector objectsBeingVisited; bool ongoingHeroMovement; bool ongoingChannelProbing; // true if AI currently explore bidirectional teleport channel exits bool havingTurn; public: AIStatus(); ~AIStatus(); void setBattle(BattleState BS); void setMove(bool ongoing); void setChannelProbing(bool ongoing); bool channelProbing(); BattleState getBattle(); void addQuery(QueryID ID, std::string description); void removeQuery(QueryID ID); int getQueriesCount(); void startedTurn(); void madeTurn(); void waitTillFree(); bool haveTurn(); void attemptedAnsweringQuery(QueryID queryID, int answerRequestID); void receivedAnswerConfirmation(int answerRequestID, int result); void heroVisit(const CGObjectInstance * obj, bool started); template void serialize(Handler & h, const int version) { h & battle; h & remainingQueries; h & requestToQueryID; h & havingTurn; } }; class DLL_EXPORT VCAI : public CAdventureAI { public: friend class FuzzyHelper; friend class ResourceManager; friend class BuildingManager; std::map> knownTeleportChannels; std::map knownSubterraneanGates; ObjectInstanceID destinationTeleport; int3 destinationTeleportPos; std::vector teleportChannelProbingList; //list of teleport channel exits that not visible and need to be (re-)explored //std::vector visitedThisWeek; //only OPWs std::map> townVisitsThisWeek; //part of mainLoop, but accessible from outisde std::vector basicGoals; Goals::TGoalVec goalsToRemove; Goals::TGoalVec goalsToAdd; std::map ultimateGoalsFromBasic; //theoreticlaly same goal can fulfill multiple basic goals std::set invalidPathHeroes; //FIXME, just a workaround std::map lockedHeroes; //TODO: allow non-elementar objectives std::map> reservedHeroesMap; //objects reserved by specific heroes std::set heroesUnableToExplore; //these heroes will not be polled for exploration in current state of game //sets are faster to search, also do not contain duplicates std::set visitableObjs; std::set alreadyVisited; std::set reservedObjs; //to be visited by specific hero std::map> visitedHeroes; //visited this turn //FIXME: this is just bug workaround AIStatus status; std::string battlename; std::shared_ptr myCb; std::unique_ptr makingTurn; ObjectInstanceID selectedObject; AIhelper * ah; VCAI(); virtual ~VCAI(); //TODO: use only smart pointers? void tryRealize(Goals::Explore & g); void tryRealize(Goals::RecruitHero & g); void tryRealize(Goals::VisitTile & g); void tryRealize(Goals::VisitObj & g); void tryRealize(Goals::VisitHero & g); void tryRealize(Goals::BuildThis & g); void tryRealize(Goals::DigAtTile & g); void tryRealize(Goals::Trade & g); void tryRealize(Goals::BuyArmy & g); void tryRealize(Goals::Invalid & g); void tryRealize(Goals::AbstractGoal & g); bool isTileNotReserved(const CGHeroInstance * h, int3 t) const; //the tile is not occupied by allied hero and the object is not reserved std::string getBattleAIName() const override; void init(std::shared_ptr CB) override; void yourTurn() override; void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; void saveGame(BinarySerializer & h, const int version) override; //saving void loadGame(BinaryDeserializer & h, const int version) override; //loading void finish() override; void availableCreaturesChanged(const CGDwelling * town) override; void heroMoved(const TryMoveHero & details) override; void heroInGarrisonChange(const CGTownInstance * town) override; void centerView(int3 pos, int focusTime) override; void tileHidden(const std::unordered_set & pos) override; void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; void artifactAssembled(const ArtifactLocation & al) override; void showTavernWindow(const CGObjectInstance * townOrTavern) override; void showThievesGuildWindow(const CGObjectInstance * obj) override; void playerBlocked(int reason, bool start) override; void showPuzzleMap() override; void showShipyardDialog(const IShipyard * obj) override; void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; void artifactPut(const ArtifactLocation & al) override; void artifactRemoved(const ArtifactLocation & al) override; void artifactDisassembled(const ArtifactLocation & al) override; void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override; void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; void tileRevealed(const std::unordered_set & pos) override; void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; void heroMovePointsChanged(const CGHeroInstance * hero) override; void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; void newObject(const CGObjectInstance * obj) override; void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; void playerBonusChanged(const Bonus & bonus, bool gain) override; void heroCreated(const CGHeroInstance *) override; void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; void showInfoDialog(const std::string & text, const std::vector & components, int soundID) override; void requestRealized(PackageApplied * pa) override; void receivedResource() override; void objectRemoved(const CGObjectInstance * obj) override; void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; void heroManaPointsChanged(const CGHeroInstance * hero) override; void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; void battleResultsApplied() override; void objectPropertyChanged(const SetObjectProperty * sop) override; void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; void showWorldViewEx(const std::vector & objectPositions) override; void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; void battleEnd(const BattleResult * br) override; void makeTurn(); void mainLoop(); void performTypicalActions(); void buildArmyIn(const CGTownInstance * t); void striveToGoal(Goals::TSubgoal ultimateGoal); Goals::TSubgoal decomposeGoal(Goals::TSubgoal ultimateGoal); void endTurn(); void wander(HeroPtr h); void setGoal(HeroPtr h, Goals::TSubgoal goal); void evaluateGoal(HeroPtr h); //evaluates goal assigned to hero, if any void completeGoal(Goals::TSubgoal goal); //safely removes goal from reserved hero void recruitHero(const CGTownInstance * t, bool throwing = false); bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, boost::optional movementCostLimit = boost::none); bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const; //void recruitCreatures(const CGTownInstance * t); void recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter); bool canGetArmy(const CGHeroInstance * h, const CGHeroInstance * source); //can we get any better stacks from other hero? void pickBestCreatures(const CArmedInstance * army, const CArmedInstance * source); //called when we can't find a slot for new stack void pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other = nullptr); void moveCreaturesToHero(const CGTownInstance * t); void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h); bool moveHeroToTile(int3 dst, HeroPtr h); void buildStructure(const CGTownInstance * t, BuildingID building); //TODO: move to BuildingManager void lostHero(HeroPtr h); //should remove all references to hero (assigned tasks and so on) void waitTillFree(); void addVisitableObj(const CGObjectInstance * obj); void markObjectVisited(const CGObjectInstance * obj); void reserveObject(HeroPtr h, const CGObjectInstance * obj); //TODO: reserve all objects that heroes attempt to visit void unreserveObject(HeroPtr h, const CGObjectInstance * obj); void markHeroUnableToExplore(HeroPtr h); void markHeroAbleToExplore(HeroPtr h); bool isAbleToExplore(HeroPtr h); void clearPathsInfo(); void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it void validateVisitableObjs(); void retrieveVisitableObjs(std::vector & out, bool includeOwned = false) const; void retrieveVisitableObjs(); virtual std::vector getFlaggedObjects() const; const CGObjectInstance * lookForArt(int aid) const; bool isAccessible(const int3 & pos) const; HeroPtr getHeroWithGrail() const; const CGObjectInstance * getUnvisitedObj(const std::function & predicate); bool isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies = false) const; //optimization - use one SM for every hero call const CGTownInstance * findTownWithTavern() const; bool canRecruitAnyHero(const CGTownInstance * t = NULL) const; Goals::TSubgoal getGoal(HeroPtr h) const; bool canAct(HeroPtr h) const; std::vector getUnblockedHeroes() const; HeroPtr primaryHero() const; void checkHeroArmy(HeroPtr h); void requestSent(const CPackForServer * pack, int requestID) override; void answerQuery(QueryID queryID, int selection); //special function that can be called ONLY from game events handling thread and will send request ASAP void requestActionASAP(std::function whatToDo); #if 0 //disabled due to issue 2890 template void registerGoals(Handler & h) { //h.template registerType(); h.template registerType(); h.template registerType(); //h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); //h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); h.template registerType(); } #endif template void serializeInternal(Handler & h, const int version) { h & knownTeleportChannels; h & knownSubterraneanGates; h & destinationTeleport; h & townVisitsThisWeek; #if 0 //disabled due to issue 2890 h & lockedHeroes; #else { ui32 length = 0; h & length; if(!h.saving) { std::set loadedPointers; lockedHeroes.clear(); for(ui32 index = 0; index < length; index++) { HeroPtr ignored1; h & ignored1; ui8 flag = 0; h & flag; if(flag) { ui32 pid = 0xffffffff; h & pid; if(!vstd::contains(loadedPointers, pid)) { loadedPointers.insert(pid); ui16 typeId = 0; //this is the problem requires such hack //we have to explicitly ignore invalid goal class type id h & typeId; Goals::AbstractGoal ignored2; ignored2.serialize(h, version); } } } } } #endif h & reservedHeroesMap; //FIXME: cannot instantiate abstract class h & visitableObjs; h & alreadyVisited; h & reservedObjs; if (version < 788 && !h.saving) { TResources saving; h & saving; //mind the ambiguity } h & status; h & battlename; h & heroesUnableToExplore; //myCB is restored after load by init call } }; class cannotFulfillGoalException : public std::exception { std::string msg; public: explicit cannotFulfillGoalException(crstring _Message) : msg(_Message) { } virtual ~cannotFulfillGoalException() throw () { }; const char * what() const throw () override { return msg.c_str(); } }; class goalFulfilledException : public std::exception { std::string msg; public: Goals::TSubgoal goal; explicit goalFulfilledException(Goals::TSubgoal Goal) : goal(Goal) { msg = goal->name(); } virtual ~goalFulfilledException() throw () { }; const char * what() const throw () override { return msg.c_str(); } }; void makePossibleUpgrades(const CArmedInstance * obj); vcmi-0.99+git20190113.f06c8a87/AI/VCAI/VCAI.vcxproj000066400000000000000000000271211342332007200203270ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 {276C3DB0-7A6B-4417-8E5C-322B08633AAC} StupidAI 10.0.17763.0 DynamicLibrary true MultiByte v140_xp DynamicLibrary true MultiByte v140_xp DynamicLibrary false true MultiByte v141 DynamicLibrary false true MultiByte v140_xp $(VCMI_Out)\AI\ $(VCMI_Out)\AI\ $(VCMI_Out)/AI $(VCMI_Out)\AI\ $(FUZZYLITEDIR) Use StdInc.h /Zm210 %(AdditionalOptions) VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) ..\..\..\libs;..\..;.. Use StdInc.h /Zm150 %(AdditionalOptions) VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) $(VCMI_Out);$(OutDir);%(AdditionalLibraryDirectories) ..\FuzzyLite\fuzzylite Use StdInc.h true MaxSpeed VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) $(VCMI_Out);$(SolutionDir)\AI /d2:-notypeopt %(AdditionalOptions) Use StdInc.h /Zm150 %(AdditionalOptions) VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) $(VCMI_Out);$(OutDir);%(AdditionalLibraryDirectories) Create Create Create Create vcmi-0.99+git20190113.f06c8a87/AI/VCAI/VCAI.vcxproj.filters000066400000000000000000000145731342332007200220050ustar00rootroot00000000000000 Pathfinding Pathfinding Pathfinding Pathfinding Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Pathfinding Pathfinding Pathfinding Pathfinding Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals Goals {f0ef4866-37a3-4a10-a6bf-34460fcefab5} {f97140a0-eee3-456f-b586-4b13265c01da} vcmi-0.99+git20190113.f06c8a87/AI/VCAI/main.cpp000066400000000000000000000012721342332007200176170ustar00rootroot00000000000000/* * main.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "VCAI.h" #ifdef __GNUC__ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif static const char * g_cszAiName = "VCAI"; extern "C" DLL_EXPORT int GetGlobalAiVersion() { return AI_INTERFACE_VER; } extern "C" DLL_EXPORT void GetAiName(char * name) { strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); } extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr & out) { out = std::make_shared(); } vcmi-0.99+git20190113.f06c8a87/AUTHORS000066400000000000000000000046641342332007200162340ustar00rootroot00000000000000VCMI PROJECT CODE CONTRIBUTORS: Michał Urbańczyk aka Tow, * project originator; programming, making releases, website maintenance, reverse engineering, general support. Mateusz B. aka Tow dragon, * general support, battle support, support for many Heroes 3 config files, reverse engineering, ERM/VERM parser and interpreter Stefan Pavlov aka Ste, * minor fixes in pregame Yifeng Sun aka phoebus118, * a part of .snd handling, minor fixes and updates Andrea Palmate aka afxgroup, * GCC/AmigaOS4 compatibility updates and makefile Vadim Glazunov aka neweagle, * minor GCC/Linux compatibility changes Rafal R. aka ambtrip, * GeniusAI (battles) Lukasz Wychrystenko aka tezeriusz, * minor GCC/Linux compatibility changes, code review Xiaomin Ding, * smack videos player Tom Zielinski aka Warmonger, * game objects, mechanics Frank Zago aka ubuntux, <> * GCC/Linux compatibility changes, sound/music support, video support on Linux Trevor Standley aka tstandley, <> * adventure map part of Genius AI Rickard Westerlund aka Onion Knight, * battle functionality and general support Ivan Savenko, * GCC/Linux support, client development, general support Benjamin Gentner aka beegee, <> * battle support, programming Alexey aka Macron1Robot, <> * minor modding changes Alexander Shishkin aka alexvins, * MinGW platform support, modding related programming Arseniy Shestakov aka SXX, * pathfinding improvements, programming Vadim Markovtsev, * resolving problems with macOS, bug fixes Michał Kalinowski, * refactoring code Dydzio, * Small features, improvements and bug fixes in all VCMI parts Piotr Wójcik aka Chocimier, * Various bug fixes Henning Koehler, * skill modding, bonus updaters Andrzej Żak aka godric3 * minor bug fixes and modding features Andrii Danylchenko * VCAI improvements vcmi-0.99+git20190113.f06c8a87/CCallback.cpp000066400000000000000000000230171342332007200174600ustar00rootroot00000000000000/* * CCallback.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CCallback.h" #include "lib/CCreatureHandler.h" #include "client/CGameInfo.h" #include "lib/CGameState.h" #include "client/CPlayerInterface.h" #include "client/Client.h" #include "lib/mapping/CMap.h" #include "lib/CBuildingHandler.h" #include "lib/mapObjects/CObjectClassesHandler.h" #include "lib/CGeneralTextHandler.h" #include "lib/CHeroHandler.h" #include "lib/NetPacks.h" #include "client/mapHandler.h" #include "lib/spells/CSpellHandler.h" #include "lib/CArtHandler.h" #include "lib/GameConstants.h" #include "lib/CPlayerState.h" #include "lib/UnlockGuard.h" #include "lib/battle/BattleInfo.h" bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where) { CastleTeleportHero pack(who->id, where->id, 1); sendRequest(&pack); return true; } bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit) { MoveHero pack(dst,h->id,transit); sendRequest(&pack); return true; } int CCallback::selectionMade(int selection, QueryID queryID) { JsonNode reply(JsonNode::JsonType::DATA_INTEGER); reply.Integer() = selection; return sendQueryReply(reply, queryID); } int CCallback::sendQueryReply(const JsonNode & reply, QueryID queryID) { ASSERT_IF_CALLED_WITH_PLAYER if(queryID == QueryID(-1)) { logGlobal->error("Cannot answer the query -1!"); return -1; } QueryReply pack(queryID, reply); pack.player = *player; return sendRequest(&pack); } void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level) { // TODO exception for neutral dwellings shouldn't be hardcoded if(player != obj->tempOwner && obj->ID != Obj::WAR_MACHINE_FACTORY && obj->ID != Obj::REFUGEE_CAMP) return; RecruitCreatures pack(obj->id, dst->id, ID, amount, level); sendRequest(&pack); } bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos) { if((player && obj->tempOwner != player) || (obj->stacksCount()<2 && obj->needsLastStack())) return false; DisbandCreature pack(stackPos,obj->id); sendRequest(&pack); return true; } bool CCallback::upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID) { UpgradeCreature pack(stackPos,obj->id,newID); sendRequest(&pack); return false; } void CCallback::endTurn() { logGlobal->trace("Player %d ended his turn.", player.get().getNum()); EndTurn pack; sendRequest(&pack); } int CCallback::swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) { ArrangeStacks pack(1,p1,p2,s1->id,s2->id,0); sendRequest(&pack); return 0; } int CCallback::mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) { ArrangeStacks pack(2,p1,p2,s1->id,s2->id,0); sendRequest(&pack); return 0; } int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) { ArrangeStacks pack(3,p1,p2,s1->id,s2->id,val); sendRequest(&pack); return 0; } bool CCallback::dismissHero(const CGHeroInstance *hero) { if(player!=hero->tempOwner) return false; DismissHero pack(hero->id); sendRequest(&pack); return true; } bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) { ExchangeArtifacts ea; ea.src = l1; ea.dst = l2; sendRequest(&ea); return true; } /** * Assembles or disassembles a combination artifact. * @param hero Hero holding the artifact(s). * @param artifactSlot The worn slot ID of the combination- or constituent artifact. * @param assemble True for assembly operation, false for disassembly. * @param assembleTo If assemble is true, this represents the artifact ID of the combination * artifact to assemble to. Otherwise it's not used. */ bool CCallback::assembleArtifacts (const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) { if (player != hero->tempOwner) return false; AssembleArtifacts aa(hero->id, artifactSlot, assemble, assembleTo); sendRequest(&aa); return true; } bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) { if(town->tempOwner!=player) return false; if(!canBuildStructure(town, buildingID)) return false; BuildStructure pack(town->id,buildingID); sendRequest(&pack); return true; } int CBattleCallback::battleMakeAction(const BattleAction * action) { assert(action->actionType == EActionType::HERO_SPELL); MakeCustomAction mca(*action); sendRequest(&mca); return 0; } int CBattleCallback::sendRequest(const CPackForServer * request) { int requestID = cl->sendRequest(request, *player); if(waitTillRealize) { logGlobal->trace("We'll wait till request %d is answered.\n", requestID); auto gsUnlocker = vstd::makeUnlockSharedGuardIf(CGameState::mutex, unlockGsWhenWaiting); CClient::waitingRequest.waitWhileContains(requestID); } boost::this_thread::interruption_point(); return requestID; } void CCallback::swapGarrisonHero( const CGTownInstance *town ) { if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player )) { GarrisonHeroSwap pack(town->id); sendRequest(&pack); } } void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) { if(hero->tempOwner != player) return; BuyArtifact pack(hero->id,aid); sendRequest(&pack); } void CCallback::trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero) { trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); } void CCallback::trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) { TradeOnMarketplace pack; pack.marketId = market->id; pack.heroId = hero ? hero->id : ObjectInstanceID(); pack.mode = mode; pack.r1 = id1; pack.r2 = id2; pack.val = val1; sendRequest(&pack); } void CCallback::setFormation(const CGHeroInstance * hero, bool tight) { SetFormation pack(hero->id,tight); sendRequest(&pack); } void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) { assert(townOrTavern); assert(hero); ui8 i=0; for(; iplayers[*player].availableHeroes.size(); i++) { if(gs->players[*player].availableHeroes[i] == hero) { HireHero pack(i, townOrTavern->id); pack.player = *player; sendRequest(&pack); return; } } } void CCallback::save( const std::string &fname ) { cl->save(fname); } void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * currentObject) { ASSERT_IF_CALLED_WITH_PLAYER PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1)); sendRequest(&pm); } void CCallback::buildBoat( const IShipyard *obj ) { BuildBoat bb; bb.objid = obj->o->id; sendRequest(&bb); } CCallback::CCallback(CGameState * GS, boost::optional Player, CClient * C) : CBattleCallback(Player, C) { gs = GS; waitTillRealize = false; unlockGsWhenWaiting = false; } CCallback::~CCallback() { //trivial, but required. Don`t remove. } bool CCallback::canMoveBetween(const int3 &a, const int3 &b) { //bidirectional return gs->map->canMoveBetween(a, b); } std::shared_ptr CCallback::getPathsInfo(const CGHeroInstance * h) { return cl->getPathsInfo(h); } int3 CCallback::getGuardingCreaturePosition(int3 tile) { if (!gs->map->isInTheMap(tile)) return int3(-1,-1,-1); return gs->map->guardingCreaturePositions[tile.x][tile.y][tile.z]; } void CCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo &out) { gs->calculatePaths(hero, out); } void CCallback::dig( const CGObjectInstance *hero ) { DigWithHero dwh; dwh.id = hero->id; sendRequest(&dwh); } void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos) { CastAdvSpell cas; cas.hid = hero->id; cas.sid = spellID; cas.pos = pos; sendRequest(&cas); } int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) { if(s1->getCreature(p1) == s2->getCreature(p2)) return mergeStacks(s1, s2, p1, p2); else return swapCreatures(s1, s2, p1, p2); } void CCallback::registerGameInterface(std::shared_ptr gameEvents) { cl->additionalPlayerInts[*player].push_back(gameEvents); } void CCallback::registerBattleInterface(std::shared_ptr battleEvents) { cl->additionalBattleInts[*player].push_back(battleEvents); } void CCallback::unregisterGameInterface(std::shared_ptr gameEvents) { cl->additionalPlayerInts[*player] -= gameEvents; } void CCallback::unregisterBattleInterface(std::shared_ptr battleEvents) { cl->additionalBattleInts[*player] -= battleEvents; } CBattleCallback::CBattleCallback(boost::optional Player, CClient *C ) { player = Player; cl = C; } bool CBattleCallback::battleMakeTacticAction( BattleAction * action ) { assert(cl->gs->curB->tacticDistance); MakeAction ma; ma.ba = *action; sendRequest(&ma); return true; } vcmi-0.99+git20190113.f06c8a87/CCallback.h000066400000000000000000000212041342332007200171210ustar00rootroot00000000000000/* * CCallback.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "lib/CGameInfoCallback.h" #include "lib/int3.h" // for int3 class CGHeroInstance; class CGameState; struct CPath; class CGObjectInstance; class CArmedInstance; class BattleAction; class CGTownInstance; struct lua_State; class CClient; class IShipyard; struct CGPathNode; struct CGPath; struct CPathsInfo; class PathfinderConfig; struct CPack; class IBattleEventsReceiver; class IGameEventsReceiver; struct ArtifactLocation; class IBattleCallback { public: bool waitTillRealize; //if true, request functions will return after they are realized by server bool unlockGsWhenWaiting;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback! //battle virtual int battleMakeAction(const BattleAction * action) = 0;//for casting spells by hero - DO NOT use it for moving active stack virtual bool battleMakeTacticAction(BattleAction * action) = 0; // performs tactic phase actions }; class IGameActionCallback { public: //hero virtual bool moveHero(const CGHeroInstance *h, int3 dst, bool transit) =0; //dst must be free, neighbouring tile (this function can move hero only by one tile) virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfuly, false - not successfuly virtual void dig(const CGObjectInstance *hero)=0; virtual void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1))=0; //cast adventure map spell //town virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)=0; virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0; virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0; virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made virtual void swapGarrisonHero(const CGTownInstance *town)=0; virtual void trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce virtual void trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; virtual int selectionMade(int selection, QueryID queryID) =0; virtual int sendQueryReply(const JsonNode & reply, QueryID queryID) =0; virtual int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//swaps creatures between two possibly different garrisons // TODO: AI-unsafe code - fix it! virtual int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//joins first stack to the second (creatures must be same type) virtual int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) =0; //first goes to the second virtual int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)=0;//split creatures from the first stack //virtual bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2)=0; //swaps artifacts between two given heroes virtual bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)=0; virtual bool assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0; virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0; virtual void endTurn()=0; virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith) virtual void setFormation(const CGHeroInstance * hero, bool tight)=0; virtual void save(const std::string &fname) = 0; virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; virtual void buildBoat(const IShipyard *obj) = 0; }; struct CPackForServer; class CBattleCallback : public IBattleCallback, public CPlayerBattleCallback { protected: int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied) CClient *cl; public: CBattleCallback(boost::optional Player, CClient *C); int battleMakeAction(const BattleAction * action) override;//for casting spells by hero - DO NOT use it for moving active stack bool battleMakeTacticAction(BattleAction * action) override; // performs tactic phase actions friend class CCallback; friend class CClient; }; class CPlayerInterface; class CCallback : public CPlayerSpecificInfoCallback, public IGameActionCallback, public CBattleCallback { public: CCallback(CGameState * GS, boost::optional Player, CClient *C); virtual ~CCallback(); //client-specific functionalities (pathfinding) virtual bool canMoveBetween(const int3 &a, const int3 &b); virtual int3 getGuardingCreaturePosition(int3 tile); virtual std::shared_ptr getPathsInfo(const CGHeroInstance * h); virtual void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out); //Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins. void registerGameInterface(std::shared_ptr gameEvents); void registerBattleInterface(std::shared_ptr battleEvents); void unregisterGameInterface(std::shared_ptr gameEvents); void unregisterBattleInterface(std::shared_ptr battleEvents); //commands bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile) bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where); int selectionMade(int selection, QueryID queryID) override; int sendQueryReply(const JsonNode & reply, QueryID queryID) override; int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) override; bool dismissHero(const CGHeroInstance * hero) override; bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override; bool assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override; void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override; bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override; bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override; void endTurn() override; void swapGarrisonHero(const CGTownInstance *town) override; void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; void trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; void trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; void setFormation(const CGHeroInstance * hero, bool tight) override; void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override; void save(const std::string &fname) override; void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; void buildBoat(const IShipyard *obj) override; void dig(const CGObjectInstance *hero) override; void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1)) override; //friends friend class CClient; }; vcmi-0.99+git20190113.f06c8a87/CI/000077500000000000000000000000001342332007200154455ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/CI/appveyor.yml000066400000000000000000000071351342332007200200430ustar00rootroot00000000000000# Common configuration for all branches version: 1.0.{build} max_jobs: 2 clone_depth: 10 clone_folder: c:\projects\vcmi\source install: - bash c:\projects\vcmi\source\CI\msvc\install.sh build_script: - cmd: c:\projects\vcmi\source\CI\msvc\build_script.bat artifacts: - path: build_$(VCMI_BUILD_PLATFORM)\*.exe notifications: - provider: Slack incoming_webhook: secure: zxT3HTnxL744HiSv7ig7sjGL4LmJ8n3MsY8PEA/kinbVMkmcxrSgVBVkHV79RfSWSyq4oLMSRvMMpG8SuDWnf6oK/qvgaiAWfwwlCIiA7uQ= # Branch-specific configuration for: # Extended configuration for toolchain_test branch - branches: only: - toolchain_test environment: matrix: - NAME: MSVS 2015 x86 - Release APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 VCMI_GENERATOR: Visual Studio 14 2015 VCMI_BUILD_PLATFORM: x86 VCMI_BUILD_CONFIGURATION: Release - NAME: MSVS 2015 x86 - Debug APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 VCMI_GENERATOR: Visual Studio 14 2015 VCMI_BUILD_PLATFORM: x86 VCMI_BUILD_CONFIGURATION: Debug - NAME: MSVS 2015 x64 - Release APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 VCMI_GENERATOR: Visual Studio 14 2015 Win64 VCMI_BUILD_PLATFORM: x64 VCMI_BUILD_CONFIGURATION: Release - NAME: MSVS 2015 x64 - Debug APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 VCMI_GENERATOR: Visual Studio 14 2015 Win64 VCMI_BUILD_PLATFORM: x64 VCMI_BUILD_CONFIGURATION: Debug - NAME: MSVS 2017 x86 - Release APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 VCMI_GENERATOR: Visual Studio 15 2017 VCMI_BUILD_PLATFORM: x86 VCMI_BUILD_CONFIGURATION: Release - NAME: MSVS 2017 x86 - Debug APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 VCMI_GENERATOR: Visual Studio 15 2017 VCMI_BUILD_PLATFORM: x86 VCMI_BUILD_CONFIGURATION: Debug - NAME: MSVS 2017 x64 - Release APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 VCMI_GENERATOR: Visual Studio 15 2017 Win64 VCMI_BUILD_PLATFORM: x64 VCMI_BUILD_CONFIGURATION: Release - NAME: MSVS 2017 x64 - Debug APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 VCMI_GENERATOR: Visual Studio 15 2017 Win64 VCMI_BUILD_PLATFORM: x64 VCMI_BUILD_CONFIGURATION: Debug # Special configuration for coverity_scan branch - branches: only: - coverity_scan environment: matrix: - NAME: Coverity - MSVS 2015 x86 - Release APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 VCMI_GENERATOR: Visual Studio 14 2015 VCMI_BUILD_PLATFORM: x86 VCMI_BUILD_CONFIGURATION: Release environment: coverity_token: secure: XNnpYevnZxGmXW1zLu+3js2S+pqfWPQmL26hVgOTBTI= coverity_email: secure: JDd5yXvYaq/yJEVjoadEhA== build_script: - cmd: c:\projects\vcmi\source\CI\msvc\coverity_build_script.bat after_test: - ps: c:\projects\vcmi\source\CI\msvc\coverity_upload_script.ps # Default configuration for all other branches - environment: matrix: - NAME: MSVS 2015 x86 - Release APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 VCMI_GENERATOR: Visual Studio 14 2015 VCMI_BUILD_PLATFORM: x86 VCMI_BUILD_CONFIGURATION: Release - NAME: MSVS 2017 x64 - Release APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 VCMI_GENERATOR: Visual Studio 15 2017 Win64 VCMI_BUILD_PLATFORM: x64 VCMI_BUILD_CONFIGURATION: Release vcmi-0.99+git20190113.f06c8a87/CI/deploy_rsa.enc000066400000000000000000000062601342332007200203010ustar00rootroot0000000000000009a` '#PPu;+!AP=;sY2z ;$n\>'C#Z3o.F-cBu)E3(so%lLF\:fZdrnJ/qa_ayԩİ:b .was+ؒc6hq7G0(p#=ciC\2>yم@otIFdŕvۡv xϾN h9﫡wOKi)G `` R.v>;.Ny3,opkZtnIvBߏ+HjJJQ!eP24١|j.x w[$FsTXHfy~D|z/ٳ!0ss/鴥 3pQ:Vy;"U;Opl%sHcI4ĕ7q/Ý3f8!ENspP#S|UȄh r]J ^B#庣*+*4;u^"xhqv0bZK>+lg.JZz۫WAȦI UJk">21:N7&#]pSkCB $q35,z2'1OiTq9 x;Iwk%uJJ-^rZi1L8Q8.-UU~e~ct:|([l1[Sn װAD2HѰAvJ|~ˈCI,zHY=:"/x]G$|r]fc AͣY:K4F'qN77' 5o΄g+-n#&P^idi}^Np֙v$F>S/+˟tU;&!y3_;:w]Kkm')U:Wz>Q峕,ioy/*a`8E'f7RPhڲ~(#JUQPb"qx{UbMT.5d_+cFQR&*Bm ڌu6>UE88ExKrpБx*\>#J y}~mkc;+ ˾x^ 5Lǩ >p4( i%.̄?y G'Hr,i[S}n2SrBt߭~Oym S])DGuƩLk2?!}̍@~M2CWM} H[ Z^aJ>h-Ǿ; o1mWAS J^ ~B- @<D ;41۾IR=A;5 wdžZ;o+t,,\$icxL"|Ԑt#1<NZ {B 3@BYZ2dW. ћxsd,g@T=(Q#;L8=䲚X /#Lx\%$`mQ(̍a6M7O:܂{0p~T O O z̐-ݻ(G>q)FsnDN &NJg-zCܦK~v5&uHrgAHD3}"{ֲkԺb d+qQAyD{#x Vt%tԮ Y؇qЌ]s,+fVʭ:ⅾ/lK,g*$bQL=O:-)ex `V 'Wkpj! 2XD Cc܏ČD9dIk_ ۫tE,j-%2k7(""B;50bϗ,2-6pkǻ&2 :pƣf.(Ṟ3ncd|q+9y'rS41Q sq!j#xL]\7=нhY-ZqiLy%qf۩QhKgo wGlQc]'o@vK!Df=ވ|`i?;->vshWEbE͵ow'%,8i vcmi) set(CMAKE_SHARED_LIBRARY_PREFIX "") if(MSVC) add_definitions(-DBOOST_ALL_NO_LIB) add_definitions(-DBOOST_ALL_DYN_LINK) set(Boost_USE_STATIC_LIBS OFF) # Don't link with SDLMain if(ENABLE_DEBUG_CONSOLE) set(SDL2_BUILDING_LIBRARY ON) add_definitions(-DVCMI_WITH_DEBUG_CONSOLE) endif() # Suppress warnings add_definitions(-D_CRT_SECURE_NO_WARNINGS) add_definitions(-D_SCL_SECURE_NO_WARNINGS) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj /wd4251") if(ENABLE_MULTI_PROCESS_BUILDS) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") endif() # Workaround: Visual Studio has issues with exports of classes that inherit templates # https://stackoverflow.com/questions/44960760/msvc-dll-exporting-class-that-inherits-from-template-cause-lnk2005-already-defin # Reported to Microsoft here: # https://developercommunity.visualstudio.com/content/problem/224597/linker-failing-because-of-multiple-definitions-of.html set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE") # Required at least for compatibility with Boost 1.68 on Vcpkg set(SYSTEM_LIBS ${SYSTEM_LIBS} bcrypt) endif(MSVC) if(MINGW) set(SYSTEM_LIBS ${SYSTEM_LIBS} ole32 oleaut32 ws2_32 mswsock dbghelp) # Check for iconv (may be needed for Boost.Locale) include(CheckLibraryExists) check_library_exists(iconv libiconv_open "" ICONV_FOUND) if(ICONV_FOUND) set(SYSTEM_LIBS ${SYSTEM_LIBS} iconv) endif() # Prevent compiler issues when building Debug # Assembler might fail with "too many sections" # With big-obj or 64-bit build will take hours if(CMAKE_BUILD_TYPE MATCHES Debug) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Og") endif() endif(MINGW) endif(WIN32) if(CMAKE_COMPILER_IS_GNUCXX OR NOT WIN32) #so far all *nix compilers support such parameters set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wall -Wextra -Wpointer-arith -Wuninitialized") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-strict-aliasing -Wno-switch -Wno-sign-compare -Wno-unused-local-typedefs") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter -Wno-overloaded-virtual -Wno-type-limits -Wno-unknown-pragmas") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder") if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-mismatched-tags -Wno-unknown-warning-option -Wno-missing-braces") endif() if(UNIX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden") set(SYSTEM_LIBS ${SYSTEM_LIBS} ${CMAKE_DL_LIBS}) endif() endif() # Check if some platform-specific libraries are needed for linking if(NOT WIN32) include(CheckLibraryExists) # Shared memory functions used by Boost.Interprocess # FindBoost handle linking with pthreads, but doesn't handle this CHECK_LIBRARY_EXISTS(rt shm_open "" HAVE_RT_LIB) if(HAVE_RT_LIB) set(SYSTEM_LIBS ${SYSTEM_LIBS} rt) endif() endif() ############################################ # Finding packages # ############################################ set(FFmpeg_FIND_COMPONENTS AVFORMAT SWSCALE) find_package(Boost 1.48.0 COMPONENTS date_time filesystem locale program_options system thread REQUIRED) find_package(ZLIB REQUIRED) find_package(FFmpeg REQUIRED) find_package(Minizip) if(MINIZIP_FOUND) add_definitions(-DUSE_SYSTEM_MINIZIP) endif() find_package(SDL2 REQUIRED) find_package(SDL2_image REQUIRED) find_package(SDL2_mixer REQUIRED) find_package(SDL2_ttf REQUIRED) if(ENABLE_LAUNCHER) # Widgets finds its own dependencies (QtGui and QtCore). find_package(Qt5Widgets REQUIRED) find_package(Qt5Network REQUIRED) endif() ############################################ # Output directories # ############################################ if(WIN32) # on Win everything goes into H3 root directory set(BIN_DIR "." CACHE STRING "Where to install binaries") set(LIB_DIR "." CACHE STRING "Where to install main library") set(DATA_DIR "." CACHE STRING "Where to install data files") elseif(APPLE) # includes lib path which determines where to install shared libraries (either /lib or /lib64) include(GNUInstallDirs) if(ENABLE_MONOLITHIC_INSTALL) set(BIN_DIR "." CACHE STRING "Where to install binaries") set(LIB_DIR "." CACHE STRING "Where to install main library") set(DATA_DIR "." CACHE STRING "Where to install data files") else() set(APP_BUNDLE_DIR "${CMAKE_PROJECT_NAME}.app") set(APP_BUNDLE_CONTENTS_DIR "${APP_BUNDLE_DIR}/Contents") set(APP_BUNDLE_BINARY_DIR "${APP_BUNDLE_CONTENTS_DIR}/MacOS") set(APP_BUNDLE_RESOURCES_DIR "${APP_BUNDLE_CONTENTS_DIR}/Resources") set(BIN_DIR "${APP_BUNDLE_BINARY_DIR}" CACHE STRING "Where to install binaries") set(LIB_DIR "${APP_BUNDLE_BINARY_DIR}" CACHE STRING "Where to install main library") set(DATA_DIR "${APP_BUNDLE_RESOURCES_DIR}/Data" CACHE STRING "Where to install data files") endif() else() # includes lib path which determines where to install shared libraries (either /lib or /lib64) include(GNUInstallDirs) if(ENABLE_MONOLITHIC_INSTALL) set(CMAKE_INSTALL_RPATH "$ORIGIN/") set(BIN_DIR "." CACHE STRING "Where to install binaries") set(LIB_DIR "." CACHE STRING "Where to install main library") set(DATA_DIR "." CACHE STRING "Where to install data files") else() if(NOT BIN_DIR) set(BIN_DIR "bin" CACHE STRING "Where to install binaries") endif() if(NOT LIB_DIR) set(LIB_DIR "${CMAKE_INSTALL_LIBDIR}/vcmi" CACHE STRING "Where to install main library") endif() if(NOT DATA_DIR) set(DATA_DIR "share/vcmi" CACHE STRING "Where to install data files") endif() endif() # following constants only used for platforms using XDG (Linux, BSD, etc) add_definitions(-DM_DATA_DIR="${CMAKE_INSTALL_PREFIX}/${DATA_DIR}") add_definitions(-DM_BIN_DIR="${CMAKE_INSTALL_PREFIX}/${BIN_DIR}") add_definitions(-DM_LIB_DIR="${CMAKE_INSTALL_PREFIX}/${LIB_DIR}") endif() set(AI_LIB_DIR "${LIB_DIR}/AI") set(SCRIPTING_LIB_DIR "${LIB_DIR}/scripting") ####################################### # Add subdirectories # ####################################### if(ENABLE_ERM) add_subdirectory(scripting/erm) endif() if(NOT MINIZIP_FOUND) add_subdirectory_with_folder("3rdparty" lib/minizip) set(MINIZIP_LIBRARIES minizip) endif() add_subdirectory(lib) add_subdirectory(client) add_subdirectory(server) add_subdirectory_with_folder("AI" AI) if(ENABLE_LAUNCHER) add_subdirectory(launcher) endif() if(ENABLE_TEST) enable_testing() add_subdirectory(test) endif() ####################################### # Installation section # ####################################### install(DIRECTORY config DESTINATION ${DATA_DIR}) install(DIRECTORY Mods DESTINATION ${DATA_DIR}) # that script is useless for Windows if(NOT WIN32) install(FILES vcmibuilder DESTINATION ${BIN_DIR} PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) endif() if(MINGW) file(GLOB dep_files ${dep_files} "${CMAKE_FIND_ROOT_PATH}/bin/*.dll") if((${CMAKE_CROSSCOMPILING}) AND (DEFINED MSYS)) message(STATUS "Detected MXE build") elseif(CMAKE_BUILD_TYPE MATCHES Debug) # Copy debug versions of libraries if build type is debug set(debug_postfix d) endif() if(ENABLE_LAUNCHER) get_target_property(QtCore_location Qt5::Core LOCATION) get_filename_component(Qtbin_folder ${QtCore_location} PATH) file(GLOB dep_files ${dep_files} ${Qtbin_folder}/Qt5Core${debug_postfix}.dll ${Qtbin_folder}/Qt5Gui${debug_postfix}.dll ${Qtbin_folder}/Qt5Widgets${debug_postfix}.dll ${Qtbin_folder}/icu*.dll) file(GLOB dep_qwindows ${Qtbin_folder}/../plugins/platforms/qwindows${debug_postfix}.dll) endif() if (ENABLE_LAUNCHER) file(GLOB dep_files ${dep_files} ${Qtbin_folder}/Qt5Network${debug_postfix}.dll) endif() install(FILES ${dep_files} DESTINATION ${BIN_DIR}) install(FILES ${dep_qwindows} DESTINATION ${BIN_DIR}/platforms) endif(MINGW) ####################################### # Packaging section # ####################################### set(CPACK_PACKAGE_VERSION_MAJOR ${VCMI_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${VCMI_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${VCMI_VERSION_PATCH}) # vcmi does not have "patch version" in version string set(CPACK_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}") #TODO: remove version from Global.h and use this one as define? set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY 0) if("${PACKAGE_NAME_SUFFIX}" STREQUAL "") set(CPACK_PACKAGE_NAME "VCMI") else() set(CPACK_PACKAGE_NAME "VCMI ${PACKAGE_NAME_SUFFIX}") endif() if("${PACKAGE_FILE_NAME}" STREQUAL "") set(CPACK_PACKAGE_FILE_NAME "vcmi-${CPACK_PACKAGE_VERSION}") else() set(CPACK_PACKAGE_FILE_NAME "${PACKAGE_FILE_NAME}") endif() set(CPACK_PACKAGE_VENDOR "VCMI team") if(WIN32) # Note: due to NSI script generation process all of the backward slashes here are useful set(CPACK_MONOLITHIC_INSTALL 1) set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/license.txt") set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}") if("${PACKAGE_NAME_SUFFIX}" STREQUAL "") set(CPACK_NSIS_PACKAGE_NAME "VCMI ${CPACK_PACKAGE_VERSION}") else() set(CPACK_NSIS_PACKAGE_NAME "VCMI ${CPACK_PACKAGE_VERSION} ${PACKAGE_NAME_SUFFIX} ") endif() set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES") if(ENABLE_LAUNCHER) set(CPACK_PACKAGE_EXECUTABLES "VCMI_launcher;VCMI") set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_launcher.exe\\\"") else() set(CPACK_PACKAGE_EXECUTABLES "VCMI_client;VCMI") set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_client.exe\\\"") endif() set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" ") # set the install/unistall icon used for the installer itself # There is a bug in NSI that does not handle full unix paths properly. set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/client\\\\vcmi.ico") set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/client\\\\vcmi.ico") # set the package header icon for MUI set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/client\\\\vcmi.ico") set(CPACK_NSIS_MENU_LINKS "http://vcmi.eu/" "VCMI Web Site") set(CPACK_NSIS_INSTALLED_ICON_NAME "VCMI_client.exe") set(CPACK_NSIS_COMPRESSOR "/SOLID lzma") set(CPACK_NSIS_DISPLAY_NAME "${CPACK_NSIS_PACKAGE_NAME}, open-source engine for Heroes of Might and Magic III ") set(CPACK_NSIS_HELP_LINK "http://vcmi.eu/") set(CPACK_NSIS_URL_INFO_ABOUT "http://vcmi.eu/") set(CPACK_NSIS_CONTACT @CPACK_PACKAGE_CONTACT@) set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") # Use BundleUtilities to fix build when Vcpkg is used and disable it for MXE if(NOT (${CMAKE_CROSSCOMPILING})) add_subdirectory(osx) endif() elseif(APPLE AND NOT ENABLE_MONOLITHIC_INSTALL) set(CPACK_MONOLITHIC_INSTALL 1) set(CPACK_GENERATOR "DragNDrop") set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_SOURCE_DIR}/osx/dmg_background.png") # CMake code for CPACK_DMG_DS_STORE executed before DS_STORE_SETUP_SCRIPT # So we can keep both enabled and this shouldn't hurt # set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/osx/dmg_DS_Store") set(CPACK_DMG_DS_STORE_SETUP_SCRIPT "${CMAKE_SOURCE_DIR}/osx/DS_Store_Setup.scpt") # Always use "VCMI" as volume name so full path will be /Volumes/VCMI/ # Otherwise DMG background image wouldn't work # Pre-generated DS_Store use absolute path to background image set(CPACK_DMG_VOLUME_NAME "${CMAKE_PROJECT_NAME}") set(MACOSX_BUNDLE_NAME "${CMAKE_PROJECT_NAME}") set(MACOSX_BUNDLE_BUNDLE_NAME "${CMAKE_PROJECT_NAME}") if(ENABLE_LAUNCHER) set(MACOSX_BUNDLE_EXECUTABLE_NAME "vcmilauncher") else() set(MACOSX_BUNDLE_EXECUTABLE_NAME "vcmiclient") endif() set(MACOSX_BUNDLE_ICON_FILE "vcmi.icns") install(FILES "${CMAKE_SOURCE_DIR}/osx/vcmi.icns" DESTINATION ${APP_BUNDLE_RESOURCES_DIR}) configure_file("${CMAKE_SOURCE_DIR}/osx/Info.plist.in" "${CMAKE_BINARY_DIR}/Info.plist") install(FILES "${CMAKE_BINARY_DIR}/Info.plist" DESTINATION ${APP_BUNDLE_CONTENTS_DIR}) # Bundle fixing code must be in separate directory to be executed after all other install code add_subdirectory(osx) else() set(CPACK_GENERATOR TGZ) endif() include(CPack) vcmi-0.99+git20190113.f06c8a87/ChangeLog000066400000000000000000001551251342332007200167350ustar00rootroot000000000000000.99 -> 1.00 GENERAL: * Spectator mode was implemented through command-line options * Some main menu settings get saved after returning to main menu - last selected map, save etc. * Restart scenario button should work correctly now * New bonuses: - SOUL_STEAL - "WoG ghost" ability, should work somewhat same as in H3 - TRANSMUTATION - "WoG werewolf"-like ability - SUMMON_GUARDIANS - "WoG santa gremlin"-like ability + two-hex unit extension - CATAPULT_EXTRA_SHOTS - defines number of extra wall attacks for units that can do so - RANGED_RETALIATION - allows ranged counterattack - BLOCKS_RANGED_RETALIATION - disallow enemy ranged counterattack - SECONDARY_SKILL_VAL2 - set additional parameter for certain secondary skills - MANUAL_CONTROL - grant manual control over war machine - WIDE_BREATH - melee creature attacks affect many nearby hexes - FIRST_STRIKE - creature counterattacks before attack if possible - SYNERGY_TARGET - placeholder bonus for Mod Design Team (subject to removal in future) - SHOOTS_ALL_ADJACENT - makes creature shots affect all neighbouring hexes - BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this - DESTRUCTION - creature ability for killing extra units after hit, configurable MULTIPLAYER: * Loading support. Save from single client could be used to load all clients. * Restart support. All clients will restart together on same server. * Hotseat mixed with network game. Multiple colors can be controlled by each client. SPELLS: * Implemented cumulative effects for spells MODS: * Improve support for WoG commander artifacts and skill descriptions * Added support for modding of original secondary skills and creation of new ones. * Map object sounds can now be configured via json * Added bonus updaters for hero specialties * Added allOf, anyOf and noneOf qualifiers for bonus limiters * Added bonus limiters: alignment, faction and terrain SOUND: * Fixed many mising or wrong pickup and visit sounds for map objects * All map objects now have ambient sounds identical to OH3 0.98 -> 0.99 GENERAL: * New Bonus NO_TERRAIN_PENALTY * Nomads will remove Sand movement penalty from army * Flying and water walking is now supported in pathfinder * New artifacts supported - Angel Wings - Boots of Levitation * Implemented rumors in tavern window * New cheat codes: - vcmiglaurung - gives 5000 crystal dragons into each slot - vcmiungoliant - conceal fog of war for current player * New console commands: - gosolo - AI take control over human players and vice versa - controlai - give control of one or all AIs to player - set hideSystemMessages on/off - supress server messages in chat BATTLES: * Drawbridge mechanics implemented (animation still missing) * Merging of town and visiting hero armies on siege implemented * Hero info tooltip for skills and mana implemented ADVENTURE AI: * Fixed AI trying to go through underground rock * Fixed several cases causing AI wandering aimlessly * AI can again pick best artifacts and exchange artifacts between heroes * AI heroes with patrol enabled won't leave patrol area anymore RANDOM MAP GENERATOR: * Changed fractalization algorithm so it can create cycles * Zones will not have straight paths anymore, they are totally random * Generated zones will have different size depending on template setting * Added Thieves Guild random object (1 per zone) * Added Seer Huts with quests that match OH3 * RMG will guarantee at least 100 pairs of Monoliths are available even if there are not enough different defs 0.97 -> 0.98 GENERAL: * Pathfinder can now find way using Monoliths and Whirlpools (only used if hero has protection) ADVENTURE AI: * AI will try to use Monolith entrances for exploration * AI will now always revisit each exit of two way monolith if exit no longer visible * AI will eagerly pick guarded and blocked treasures ADVENTURE MAP: * Implemented world view * Added graphical fading effects SPELLS: * New spells handled: - Earthquake - View Air - View Earth - Visions - Disguise * Implemented CURE spell negative dispell effect * Added LOCATION target for spells castable on any hex with new target modifiers BATTLES: * Implemented OH3 stack split / upgrade formulas according to AlexSpl RANDOM MAP GENERATOR: * Underground tunnels are working now * Implemented "junction" zone type * Improved zone placing algorithm * More balanced distribution of treasure piles * More obstacles within zones 0.96 -> 0.97 (Nov 01 2014) GENERAL: * (windows) Moved VCMI data directory from '%userprofile%\vcmi' to '%userprofile%\Documents\My Games\vcmi' * (windows) (OSX) Moved VCMI save directory from 'VCMI_DATA\Games' to 'VCMI_DATA\Saves' * (linux) * Changes in used librries: - VCMI can now be compiled with SDL2 - Movies will use ffmpeg library - change boost::bind to std::bind - removed boost::asign - Updated FuzzyLite to 5.0 * Multiplayer load support was implemented through command-line options ADVENTURE AI: * Significantly optimized execution time, AI should be much faster now. ADVENTURE MAP: * Non-latin characters can now be entered in chat window or used for save names. * Implemented separate speed for owned heroes and heroes owned by other players GRAPHICS: * Better upscaling when running in fullscreen mode. * New creature/commader window * New resolutions and bonus icons are now part of a separate mod * Added graphics for GENERAL_DAMAGE_REDUCTION bonus (Kuririn) RANDOM MAP GENERATOR: * Random map generator now creates complete and playable maps, should match original RMG * All important features from original map templates are implemented * Fixed major crash on removing objects * Undeground zones will look just like surface zones LAUNCHER: * Implemented switch to disable intro movies in game 0.95 -> 0.96 (Jul 01 2014) GENERAL: * (linux) now VCMI follows XDG specifications. See http://forum.vcmi.eu/viewtopic.php?t=858 ADVENTURE AI: * Optimized speed and removed various bottlenecks. ADVENTURE MAP: * Heroes auto-level primary and secondary skill levels according to experience BATTLES: * Wall hit/miss sound will be played when using catapult during siege SPELLS: * New configuration format: http://wiki.vcmi.eu/index.php?title=Spell_Format RANDOM MAP GENERATOR * Towns from mods can be used * Reading connections, terrains, towns and mines from template * Zone placement * Zone borders and connections, fractalized paths inside zones * Guard generation * Treasue piles generation (so far only few removable objects) MODS: * Support for submods - mod may have their own "submods" located in /Mods directory * Mods may provide their own changelogs and screenshots that will be visible in Launcher * Mods can now add new (offensive, buffs, debuffs) spells and change existing * Mods can use custom mage guild background pictures and videos for taverns, setting of resources daily income for buildings GENERAL: * Added configuring of heroes quantity per player allowed in game 0.94 -> 0.95 (Mar 01 2014) GENERAL: * Components of combined artifacts will now display info about entire set. * Implements level limit * Added WoG creature abilities by Kuririn * Implemented a confirmation dialog when pressing Alt + F4 to quit the game * Added precompiled header compilation for CMake (can be enabled per flag) * VCMI will detect changes in text files using crc-32 checksum * Basic support for unicode. Internally vcmi always uses utf-8 * (linux) Launcher will be available as "VCMI" menu entry from system menu/launcher * (linux) Added a SIGSEV violation handler to vcmiserver executable for logging stacktrace (for convenience) ADVENTURE AI: More info at http://wiki.vcmi.eu/index.php?title=Adventure_AI * AI will use fuzzy logic to compare and choose multiple possible subgoals. * AI will now use SectorMap to find a way to guarded / covered objects. * Significantly improved exploration algorithm. * Locked heroes now try to decompose their goals exhaustively. * Fixed (common) issue when AI found neutral stacks infinitely strong. * Improvements for army exchange criteria. * GatherArmy may include building dwellings in town (experimental). * AI should now conquer map more aggressively and much faster * Fuzzy rules will be printed out at map launch (if AI log is enabled) CAMPAIGNS: * Implemented move heroes to next scenario * Support for non-standard victory conditions for H3 campaigns * Campaigns use window with bonus & scenario selection than scenario information window from normal maps * Implemented hero recreate handling (e.g. Xeron will be recreated on AB campaign) * Moved place bonus hero before normal random hero and starting hero placement -> same behaviour as in OH3 * Moved placing campaign heroes before random object generation -> same behaviour as in OH3 TOWNS: * Extended building dependencies support MODS: * See http://wiki.vcmi.eu/index.php?title=Modding_changelog#0.94_-.3E_0.95 for format changes * Custom victory/loss conditions for maps or campaigns * 7 days without towns loss condition is no longer hardcoded * Only changed mods will be validated 0.93 -> 0.94 (Oct 01 2013) GENERAL: * New Launcher application, see * Filesystem now supports zip archives. They can be loaded similarly to other archives in filesystem.json. Mods can use Content.zip instead of Content/ directory. * fixed "get txt" console command * command "extract" to extract file by name * command "def2bmp" to convert def into set of frames. * fixed crash related to cammander's SPELL_AFTER_ATTACK spell id not initialized properly (text id was resolved on copy of bonus) * fixed duels, added no-GUI mode for automatic AI testing * Sir Mullich is available at the start of the game * Upgrade cost will never be negative. * support for Chinese fonts (GBK 2-byte encoding) ADVENTURE MAP * if Quick Combat option is turned on, battles will be resolved by AI * first hero is awakened on new turn * fixed 3000 gems reward in shipwreck BATTLES: * autofight implemented * most of the animations is time-based * simplified postioning of units in battle, should fix remaining issues with unit positioning * synchronized attack/defence animation * spell animation speed uses game settings * fixed disrupting ray duration * added logging domain for battle animations * Fixed crashes on Land Mines / Fire Wall casting. * UI will be correctly greyed-out during opponent turn * fixed remaining issues with blit order * Catapult attacks should be identical to H3. Catapult may miss and attack another part of wall instead (this is how it works in H3) * Fixed Remove Obstacle. *defeating hero will yield 500 XP * Added lots of missing spell immunities from Strategija * Added stone gaze immunity for Troglodytes (did you know about it?) * damage done by turrets is properly increased by built buldings * Wyverns will cast Poison instead of Stone Gaze. TOWN: * Fixed issue that allowed to build multiple boats in town. * fix for lookout tower 0.92 -> 0.93 (Jun 01 2013) GENERAL: * Support for SoD-only installations, WoG becomes optional addition * New logging framework * Negative luck support, disabled by default * Several new icons for creature abilities (Fire Shield, Non-living, Magic Mirror, Spell-like Attack) * Fixed stack artifact (and related buttons) not displaying in creature window. * Fixed crash at month of double population. MODS: * See http://wiki.vcmi.eu/index.php?title=Modding_changelog#0.92_-.3E_0.93 for compatibility info. * Improved json validation. Now it support most of features from latest json schema draft. * Icons use path to icon instead of image indexes. * It is possible to edit data of another mod or H3 data via mods. * Mods can access only ID's from dependenies, virtual "core" mod and itself (optional for some mods compatibility) * Removed no longer needed field "projectile spins" * Heroes: split heroes.json in manner similar to creatures\factions; string ID's for H3 heroes; h3 hero classes and artifacts can be modified via json. BATTLES: * Fixed Death Stare of Commanders * Projectile blitting should be closer to original H3. But still not perfect. * Fixed missing Mirth effects * Stack affected by Berserk should not try to attack itself * Fixed several cases of incorrect positioning of creatures in battles * Fixed abilities of Efreet. * Fixed broken again palette in some battle backgrounds TOWN: * VCMI will not crash if building selection area is smaller than def * Detection of transparency on selection area is closer to H3 * Improved handling buildings with mode "auto": - they will be properly processed (new creatures will be added if dwelling, spells learned if mage guild, and so on) - transitive dependencies are handled (A makes B build, and B makes C and D) SOUND: * Added missing WoG creature sounds (from Kuririn). * The Windows package comes with DLLs needed to play .ogg files * (linux) convertMP3 option for vcmibuilder for systems where SDL_Mixer can't play mp3's * some missing sounds for battle effects ARTIFACTS: * Several fixes to combined artifacts added via mods. * Fixed Spellbinder's Hat giving level 1 spells instead of 5. * Fixed incorrect components of Cornucopia. * Cheat code with grant all artifacts, including the ones added by mods 0.91 -> 0.92 (Mar 01 2013) GENERAL: * hero crossover between missions in campaigns * introduction before missions in campaigns MODS: * Added CREATURE_SPELL_POWER for commanders * Added spell modifiers to various spells: Hypnotize (Astral), Firewall (Luna), Landmine * Fixed ENEMY_DEFENCE_REDUCTION, GENERAL_ATTACK_REDUCTION * Extended usefulness of ONLY_DISTANCE_FIGHT, ONLY_MELEE_FIGHT ranges * Double growth creatures are configurable now * Drain Life now has % effect depending on bonus value * Stack can use more than 2 attacks. Additional attacks can now be separated as "ONLY_MELEE_FIGHT and "ONLY_DISTANCE_FIGHT". * Moat damage configurable * More config options for spells: - mind immunity handled by config - direct damage immunity handled by config - immunity icon configurable - removed mind_spell flag * creature config use string ids now. * support for string subtype id in short bonus format * primary skill identifiers for bonuses 0.9 -> 0.91 (Feb 01 2013) GENERAL: * VCMI build on OS X is now supported * Completely removed autotools * Added RMG interace and ability to generate simplest working maps * Added loading screen MODS: - Simplified mod structure. Mods from 0.9 will not be compatible. * Mods can be turned on and off in config/modSettings.json file * Support for new factions, including: - New towns - New hero classes - New heroes - New town-related external dwellings * Support for new artifact, including combined, commander and stack artifacts * Extended configuration options - All game objects are referenced by string identifiers - Subtype resolution for bonuses BATTLES: * Support for "enchanted" WoG ability ADVENTURE AI: * AI will try to use Subterranean Gate, Redwood Observatory and Cartographer for exploration * Improved exploration algorithm * AI will prioritize dwellings and mines when there are no opponents visible 0.89 -> 0.9 (Oct 01 2012) GENERAL: * Provisional support creature-adding mods * New filesystem allowing easier resource adding/replacing * Reorganized package for better compatibility with HotA and not affecting the original game * Moved many hard-coded settings into text config files * Commander level-up dialog * New Quest Log window * Fixed a number of bugs in campaigns, support for starting hero selection bonus. BATTLES * New graphics for Stack Queue * Death Stare works identically to H3 * No explosion when catapult fails to damage the wall * Fixed crash when attacking stack dies before counterattack * Fixed crash when attacking stack dies in the Moat just before the attack * Fixed Orb of Inhibition and Recanter's Cloak (they were incorrectly implemented) * Fleeing hero won't lose artifacts. * Spellbook won't be captured. ADVENTURE AI * support for quests (Seer Huts, Quest Guardians, and so) * AI will now wander with all the heroes that have spare movement points. It should prevent stalling. * AI will now understand threat of Abandoned Mine. * AI can now exchange armies between heroes. By default, it will pass army to main hero. * Fixed strange case when AI found allied town extremely dangerous * Fixed crash when AI tried to "revisit" a Boat * Fixed crash when hero assigned to goal was lost when attempting realizing it * Fixed a possible freeze when exchanging resources at marketplace BATTLE AI * It is possible to select a battle AI module used by VCMI by typing into the console "setBattleAI ". The names of avaialble modules are "StupidAI" and "BattleAI". BattleAI may be a little smarter but less stable. By the default, StupidAI will be used, as in previous releases. * New battle AI module: "BattleAI" that is smarter and capable of casting some offensive and enchantment spells 0.88 -> 0.89 (Jun 01 2012) GENERAL * Mostly implemented Commanders feature (missing level-up dialog) * Support for stack artifacts * New creature window graphics contributed by fishkebab * Config file may have multiple upgrades for creatures * CTRL+T will open marketplace window * G will open thieves guild window if player owns at least one town with tavern * Implemented restart functionality. CTRL+R will trigger a quick restart * Save game screen and returning to main menu will work if game was started with --start option * Simple mechanism for detecting game desynchronization after init * 1280x800 resolution graphics, contributed by Topas ADVENTURE MAP * Fixed monsters regenerating casualties from battle at the start of new week. * T in adventure map will switch to next town BATTLES * It's possible to switch active creature during tacts phase by clicking on stack * After battle artifacts of the defeated hero (and his army) will be taken by winner * Rewritten handling of battle obstacles. They will be now placed following H3 algorithm. * Fixed crash when death stare or acid breath activated on stack that was just killed * First aid tent can heal only creatures that suffered damage * War machines can't be healed by tent * Creatures casting spells won't try to cast them during tactic phase * Console tooltips for first aid tent * Console tooltips for teleport spell * Cursor is reset to pointer when action is requested * Fixed a few other missing or wrong tooltips/cursors * Implemented opening creature window by l-clicking on stack * Fixed crash on attacking walls with Cyclop Kings * Fixed and simplified Teleport casting * Fixed Remove Obstacle spell * New spells supported: - Chain Lightning - Fire Wall - Force Field - Land Mine - Quicksands - Sacrifice TOWNS: * T in castle window will open a tavern window (if available) PREGAME * Pregame will use same resolution as main game * Support for scaling background image * Customization of graphics with config file. ADVENTURE AI * basic rule system for threat evaluation * new town development logic * AI can now use external dwellings * AI will weekly revisit dwellings & mills * AI will now always pick best stacks from towns * AI will recruit multiple heroes for exploration * AI won't try attacking its own heroes 0.87 -> 0.88 (Mar 01 2012) * added an initial version of new adventure AI: VCAI * system settings window allows to change default resolution * introduced unified JSON-based settings system * fixed all known localization issues * Creature Window can handle descriptions of spellcasting abilities * Support for the clone spell 0.86 -> 0.87 (Dec 01 2011) GENERAL: * Pathfinder can find way using ships and subterranean gates * Hero reminder & sleep button PREGAME: * Credits are implemented BATTLES * All attacked hexes will be highlighted * New combat abilities supported: - Spell Resistance aura - Random spellcaster (Genies) - Mana channeling - Daemon summoning - Spellcaster (Archangel Ogre Mage, Elementals, Faerie Dragon) - Fear - Fearless - No wall penalty - Enchanter - Bind - Dispell helpful spells 0.85 -> 0.86 (Sep 01 2011) GENERAL: * Reinstated music support * Bonus system optimizations (caching) * converted many config files to JSON * .tga file support * New artifacts supported - Admiral's Hat - Statue of Legion - Titan's Thunder BATTLES: * Correct handling of siege obstacles * Catapult animation * New combat abilities supported - Dragon Breath - Three-headed Attack - Attack all around - Death Cloud / Fireball area attack - Death Blow - Lightning Strike - Rebirth * New WoG abilities supported - Defense Bonus - Cast before attack - Immunity to direct damage spells * New spells supported - Magic Mirror - Titan's Lightning Bolt 0.84 -> 0.85 (Jun 01 2011) GENERAL: * Support for stack experience * Implemented original campaign selection screens * New artifacts supported: - Statesman's Medal - Diplomat's Ring - Ambassador's Sash TOWNS: * Implemented animation for new town buildings * It's possible to sell artifacts at Artifact Merchants BATTLES: * Neutral monsters will be split into multiple stacks * Hero can surrender battle to keep army * Support for Death Stare, Support for Poison, Age, Disease, Acid Breath, Fire / Water / Earth / Air immunities and Receptiveness * Partial support for Stone Gaze, Paralyze, Mana drain 0.83 -> 0.84 (Mar 01 2011) GENERAL: * Bonus system has been rewritten * Partial support for running VCMI in duel mode (no adventure map, only one battle, ATM only AI-AI battles) * New artifacts supported: - Angellic Alliance - Bird of Perception - Emblem of Cognizance - Spell Scroll - Stoic Watchman BATTLES: * Better animations handling * Defensive stance is supported HERO: * New secondary skills supported: - Artillery - Eagle Eye - Tactics AI PLAYER: * new AI leading neutral creatures in combat, slightly better then previous 0.82 -> 0.83 (Nov 01 2010) GENERAL: * Alliances support * Week of / Month of events * Mostly done pregame for MP games (temporarily only for local clients) * Support for 16bpp displays * Campaigns: - support for building bonus - moving to next map after victory * Town Portal supported * Vial of Dragon Blood and Statue of Legion supported HERO: * remaining specialities have been implemented TOWNS: * town events supported * Support for new town structures: Deiety of Fire and Escape Tunnel BATTLES: * blocked retreating from castle 0.81 -> 0.82 (Aug 01 2010) GENERAL: * Some of the starting bonuses in campaigns are supported * It's possible to select difficulty level of mission in campaign * new cheat codes: - vcmisilmaril - player wins - vcmimelkor - player loses ADVENTURE MAP: * Neutral armies growth implemented (10% weekly) * Power rating of neutral stacks * Favourable Winds reduce sailing cost HERO: * Learning secondary skill supported. * Most of hero specialities are supported, including: -Creature specialities (progressive, fixed, Sir Mullich) -Spell damage specialities (Deemer), fixed bonus (Ciele) -Secondary skill bonuses -Creature Upgrades (Gelu) -Resorce generation -Starting Skill (Adrienne) TOWNS: * Support for new town structures: - Artifact Merchant - Aurora Borealis - Castle Gates - Magic University - Portal of Summoning - Skeleton transformer - Veil of Darkness OBJECTS: * Stables will now upgrade Cavaliers to Champions. New object supported: - Abandoned Mine - Altar of Sacrifice - Black Market - Cover of Darkness - Hill Fort - Refugee Camp - Sanctuary - Tavern - University - Whirlpool 0.8 -> 0.81 (Jun 01 2010) GENERAL: * It's possible to start campaign * Support for build grail victory condition * New artifacts supported: - Angel's Wings - Boots of levitation - Orb of Vulnerability - Ammo cart - Golden Bow - Hourglass of Evil Hour - Bow of Sharpshooter - Armor of the Damned ADVENTURE MAP: * Creatures now guard surrounding tiles * New adventura map spells supported: - Summon Boat - Scuttle Boat - Dimension Door - Fly - Water walk BATTLES: * A number of new creature abilities supported * First Aid Tent is functional * Support for distance/wall/melee penalties & no * penalty abilities * Reworked damage calculation to fit OH3 formula better * Luck support * Teleportation spell HERO: * First Aid secondary skill * Improved formula for necromancy to match better OH3 TOWNS: * Sending resources to other players by marketplace * Support for new town structures: - Lighthouse - Colossus - Freelancer's Guild - Guardian Spirit - Necromancy Amplifier - Soul Prison OBJECTS: New object supported: - Freelancer's Guild - Trading Post - War Machine Factory 0.75 -> 0.8 (Mar 01 2010) GENERAL: * Victory and loss conditions are supported. It's now possible to win or lose the game. * Implemented assembling and disassembling of combination artifacts. * Kingdom Overview screen is now available. * Implemented Grail (puzzle map, digging, constructing ultimate building) * Replaced TTF fonts with original ones. ADVENTURE MAP: * Implemented rivers animations (thx to GrayFace). BATTLES: * Fire Shield spell (and creature ability) supported * affecting morale/luck and casting spell after attack creature abilities supported HERO: * Implementation of Scholar secondary skill TOWN: * New left-bottom info panel functionalities. TOWNS: * new town structures supported: - Ballista Yard - Blood Obelisk - Brimstone Clouds - Dwarven Treasury - Fountain of Fortune - Glyphs of Fear - Mystic Pond - Thieves Guild - Special Grail functionalities for Dungeon, Stronghold and Fortress OBJECTS: New objects supported: - Border gate - Den of Thieves - Lighthouse - Obelisk - Quest Guard - Seer hut A lot of of various bugfixes and improvements: http://bugs.vcmi.eu/changelog_page.php?version_id=14 0.74 -> 0.75 (Dec 01 2009) GENERAL: * Implemented "main menu" in-game option. * Hide the mouse cursor while displaying a popup window. * Better handling of huge and empty message boxes (still needs more changes) * Fixed several crashes when exiting. ADVENTURE INTERFACE: * Movement cursor shown for unguarded enemy towns. * Battle cursor shown for guarded enemy garrisons. * Clicking on the border no longer opens an empty info windows HERO WINDOW: * Improved artifact moving. Available slots are higlighted. Moved artifact is bound to mouse cursor. TOWNS: * new special town structures supported: - Academy of Battle Scholars - Cage of Warlords - Mana Vortex - Stables - Skyship (revealing entire map only) OBJECTS: * External dwellings increase town growth * Right-click info window for castles and garrisons you do not own shows a rough amount of creatures instead of none * Scholar won't give unavaliable spells anymore. A lot of of various bugfixes and improvements: http://bugs.vcmi.eu/changelog_page.php?version_id=2 0.73 -> 0.74 (Oct 01 2009) GENERAL: * Scenario Information window * Save Game window * VCMI window should start centered * support for Necromancy and Ballistics secondary skills * new artifacts supported, including those improving Necromancy, Legion Statue parts, Shackles of War and most of combination artifacts (but not combining) * VCMI client has its own icon (thx for graphic to Dikamilo) * Ellipsis won't be split when breaking text on several lines * split button will be grayed out when no creature is selected * fixed issue when splitting stack to the hero with only one creatures * a few fixes for shipyard window ADVENTURE INTERFACE: * Cursor shows if tile is accesible and how many turns away * moving hero with arrow keys / numpad * fixed Next Hero button behaviour * fixed Surface/Underground switch button in higher resolutions BATTLES: * partial siege support * new stack queue for higher resolutions (graphics made by Dru, thx!) * 'Q' pressing toggles the stack queue displaying (so it can be enabled/disabled it with single key press) * more creatures special abilities supported * battle settings will be stored * fixed crashes occurring on attacking two hex creatures from back * fixed crash when clicking on enemy stack without moving mouse just after receiving action * even large stack numbers will fit the boxes * when active stack is killed by spell, game behaves properly * shooters attacking twice (like Grand Elves) won't attack twice in melee * ballista can shoot even if there's an enemy creature next to it * improved obstacles placement, so they'll better fit hexes (thx to Ivan!) * selecting attack directions works as in H3 * estimating damage that will be dealt while choosing stack to be attacked * modified the positioning of battle effects, they should look about right now. * after selecting a spell during combat, l-click is locked for any action other than casting. * flying creatures will be blitted over all other creatures, obstacles and wall * obstacles and units should be printed in better order (not tested) * fixed armageddon animation * new spells supported: - Anti-Magic - Cure - Resurrection - Animate Dead - Counterstrike - Berserk - Hypnotize - Blind - Fire Elemental - Earth Elemental - Water Elemental - Air Elemental - Remove obstacle TOWNS: * enemy castle can be taken over * only one capitol per player allowed (additional ones will be lost) * garrisoned hero can buy a spellbook * heroes available in tavern should be always different * ship bought in town will be correctly placed * new special town structures supported: - Lookout Tower - Temple of Valhalla - Wall of Knowledge - Order of Fire HERO WINDOW: * war machines cannot be unequiped PREGAME: * sorting: a second click on the column header sorts in descending order. * advanced options tab: r-click popups for selected town, hero and bonus * starting scenario / game by double click * arrows in options tab are hidden when not available * subtitles for chosen hero/town/bonus in pregame OBJECTS: * fixed pairing Subterranean Gates New objects supported: - Borderguard & Keymaster Tent - Cartographer - Creature banks - Eye of the Magi & Hut of the Magi - Garrison - Stables - Pandora Box - Pyramid 0.72 -> 0.73 (Aug 01 2009) GENERAL: * infowindow popup will be completely on screen * fixed possible crash with in game console * fixed crash when gaining artifact after r-click on hero in tavern * Estates / hero bonuses won't give resources on first day. * video handling (intro, main menu animation, tavern animation, spellbook animation, battle result window) * hero meeting window allowing exchanging armies and artifacts between heroes on adventure map * 'T' hotkey opens marketplace window * giving starting spells for heroes * pressing enter or escape close spellbook * removed redundant quotation marks from skills description and artifact events texts * disabled autosaving on first turn * bonuses from bonus artifacts * increased char per line limit for subtitles under components * corrected some exp/level values * primary skills cannot be negative * support for new artifacts: Ring of Vitality, Ring of Life, Vial of Lifeblood, Garniture of Interference, Surcoat of Counterpoise, Boots of Polarity * fixed timed events reappearing * saving system options * saving hero direction * r-click popups on enemy heroes and towns * hero leveling formula matches the H3 ADVENTURE INTERFACE: * Garrisoning, then removing hero from garrison move him at the end of the heroes list * The size of the frame around the map depends on the screen size. * spellbook shows adventure spells when opened on adventure map * erasing path after picking objects with last movement point BATTLES: * spell resistance supported (secondary skill, artifacts, creature skill) * corrected damage inflicted by spells and ballista * added some missing projectile infos * added native terrain bonuses in battles * number of units in stack in battle should better fit the box * non-living and undead creatures have now always 0 morale * displaying luck effect animation * support for battleground overlays: - cursed ground - magic plains - fiery fields - rock lands - magic clouds - lucid pools - holy ground - clover field - evil fog TOWNS: * fixes for horde buildings * garrisoned hero can buy a spellbook if he is selected or if there is no visiting hero * capitol bar in town hall is grey (not red) if already one exists * fixed crash on entering hall when town was near map edge HERO WINDOW: * garrisoned heroes won't be shown on the list * artifacts will be present on morale/luck bonuses list PREGAME: * saves are sorted primary by map format, secondary by name * fixed displaying date of saved game (uses local time, removed square character) OBJECTS: * Fixed primary/secondary skill levels given by a scholar. * fixed problems with 3-tiles monoliths * fixed crash with flaggable building next to map edge * fixed some descriptions for events * New objects supported: - Buoy - Creature Generators - Flotsam - Mermaid - Ocean bottle - Sea Chest - Shipwreck Survivor - Shipyard - Sirens 0.71 -> 0.72 (Jun 1 2009) GENERAL: * many sound effects and music * autosave (to 5 subsequent files) * artifacts support (most of them) * added internal game console (activated on TAB) * fixed 8 hero limit to check only for wandering heroes (not garrisoned) * improved randomization * fixed crash on closing application * VCMI won't always give all three stacks in the starting armies * fix for drawing starting army creatures count * Diplomacy secondary skill support * timed events won't cause resources amount to be negative * support for sorcery secondary skill * reduntant quotation marks from artifact descriptions are removed * no income at the first day ADVENTURE INTERFACE: * fixed crasbug occurring on revisiting objects (by pressing space) * always restoring default cursor when movng mouse out of the terrain * fixed map scrolling with ctrl+arrows when some windows are opened * clicking scrolling arrows in town/hero list won't open town/hero window * pathfinder will now look for a path going via printed positions of roads when it's possible * enter can be used to open window with selected hero/town BATTLES: * many creatures special skills implemented * battle will end when one side has only war machines * fixed some problems with handling obstacles info * fixed bug with defending / waiting while no stack is active * spellbook button is inactive when hero cannot cast any spell * obstacles will be placed more properly when resolution is different than 800x600 * canceling of casting a spell by pressing Escape or R-click (R-click on a creatures does not cancel a spell) * spellbook cannot be opened by L-click on hero in battle when it shouldn't be possible * new spells: - frost ring - fireball - inferno - meteor shower - death ripple - destroy undead - dispel - armageddon - disrupting ray - protection from air - protection from fire - protection from water - protection from earth - precision - slayer TOWNS: * resting in town with mage guild will replenih all the mana points * fixed Blacksmith * the number of creatures at the beginning of game is their base growth * it's possible to enter Tavern via Brotherhood of Sword HERO WINDOW: * fixed mana limit info in the hero window * war machines can't be removed * fixed problems with removing artifacts when all visible slots in backpack are full PREGAME: * clicking on "advanced options" a second time now closes the tab instead of refreshing it. * Fix position of maps names. * Made the slider cursor much more responsive. Speedup the map select screen. * Try to behave when no maps/saves are present. * Page Up / Page Down / Home / End hotkeys for scrolling through scenarios / games list OBJECTS: * Neutral creatures can join or escape depending on hero strength (escape formula needs to be improved) * leaving guardians in flagged mines. * support for Scholar object * support for School of Magic * support for School of War * support for Pillar of Fire * support for Corpse * support for Lean To * support for Wagon * support for Warrior's Tomb * support for Event * Corpse (Skeleton) will be accessible from all directions 0.7 -> 0.71 (Apr 01 2009) GENERAL: * fixed scrolling behind window problem (now it's possible to scroll with CTRL + arrows) * morale/luck system and corresponding sec. skills supported * fixed crash when hero get level and has less than two sec. skills to choose between * added keybindings for components in selection window (eg. for treasure chest dialog): 1, 2, and so on. Selection dialog can be closed with Enter key * proper handling of custom portraits of heroes * fixed problems with non-hero/town defs not present in def list but present on map (occurring probably only in case of def substitution in map editor) * fixed crash when there was no hero available to hire for some player * fixed problems with 1024x600 screen resolution * updating blockmap/visitmap of randomized objects * fixed crashes on loading maps with flag all mines/dwelling victory condition * further fixes for leveling-up (stability and identical offered skills bug) * splitting window allows to rebalance two stack with the same creatures * support for numpad keyboard * support for timed events ADVENTURE INTERFACE: * added "Next hero" button functionality * added missing path arrows * corrected centering on hero's position * recalculating hero path after reselecting hero * further changes in pathfinder making it more like original one * orientation of hero can't be change if movement points are exhausted * campfire, borderguard, bordergate, questguard will be accessible from the top * new movement cost calculation algorithm * fixed sight radious calculation * it's possible to stop hero movement * faster minimap refreshing * provisional support for "Save" button in System Options Window * it's possible to revisit object under hero by pressing Space BATTLES: * partial support for battle obstacles * only one spell can be casted per turn * blocked opening sepllbook if hero doesn't have a one * spells not known by hero can't be casted * spell books won't be placed in War Machine slots after battle * attack is now possible when hex under cursor is not displayed * glowing effect of yellow border around creatures * blue glowing border around hovered creature * made animation on battlefield more smooth * standing stacks have more static animation * probably fixed problem with displaying corpses on battlefield * fixes for two-hex creatures actions * fixed hero casting spell animation * corrected stack death animation * battle settings will be remembered between battles * improved damage calculation formula * correct handling of flying creatures in battles * a few tweaks in battle path/available hexes calculation (more of them is needed) * amounts of units taking actions / being an object of actions won't be shown until action ends * fixed positions of stack queue and battle result window when resolution is != 800x600 * corrected duration of frenzy spell which was incorrect in certain cases * corrected hero spell casting animation * better support for battle backgrounds * blocked "save" command during battle * spellbook displays only spells known by Hero New spells supported: - Mirth - Sorrow - Fortune - Misfortune TOWN INTERFACE: * cannot build more than one capitol * cannot build shipyard if town is not near water * Rampart's Treasury requires Miner's Guild * minor improvements in Recruitment Window * fixed crash occurring when clicking on hero portrait in Tavern Window, minor improvements for Tavern Window * proper updating resdatabar after building structure in town or buying creatures (non 800x600 res) * fixed blinking resdatabar in town screen when buying (800x600) * fixed horde buildings displaying in town hall * forbidden buildings will be shown as forbidden, even if there are no res / other conditions are not fulfilled PREGAME: * added scrolling scenario list with mouse wheel * fixed mouse slow downs * cannot select heroes for computer player (pregame) * no crash if uses gives wrong resolution ID number * minor fixes OBJECTS: * windmill gives 500 gold only during first week ever (not every month) * After the first visit to the Witch Hut, right-click/hover tip mentions the skill available. New objects supported: * Prison * Magic Well * Faerie Ring * Swan Pond * Idol of Fortune * Fountain of Fortune * Rally Flag * Oasis * Temple * Watering Hole * Fountain of Youth * support for Redwood Observatory * support for Shrine of Magic Incantation / Gesture / Thought * support for Sign / Ocean Bottle AI PLAYER: Minor improvements and fixes. 0.64 -> 0.7 (Feb 01 2009) GENERAL: * move some settings to the config/settings.txt file * partial support for new screen resolutions * it's possible to set game resolution in pregame (type 'resolution' in the console) * /Data and /Sprites subfolders can be used for adding files not present in .lod archives * fixed crashbug occurring when hero levelled above 15 level * support for non-standard screen resolutions * F4 toggles between full-screen and windowed mode * minor improvements in creature card window * splitting stacks with the shift+click * creature card window contains info about modified speed ADVENTURE INTERFACE: * added water animation * speed of scrolling map and hero movement can be adjusted in the System Options Window * partial handling r-clicks on adventure map TOWN INTERFACE: * the scroll tab won't remain hanged to our mouse position if we move the mouse is away from the scroll bar * fixed cloning creatures bug in garrisons (and related issues) BATTLES * support for the Wait command * magic arrow *really* works * war machines support partially added * queue of stacks narrowed * spell effect animation displaying improvements * positive/negative spells cannot be cast on hostile/our stacks * showing spell effects affecting stack in creature info window * more appropriate coloring of stack amount box when stack is affected by a spell * battle console displays notifications about wait/defend commands * several reported bugs fixed * new spells supported: a) Haste b) lightning bolt c) ice bolt d) slow e) implosion f) forgetfulness g) shield h) air shield i) bless j) curse k) bloodlust l) weakness m) stone skin n) prayer o) frenzy AI PLAYER: * Genius AI (first VCMI AI) will control computer creatures during the combat. OBJECTS: * Guardians property for resources is handled * support for Witch Hut * support for Arena * support for Library of Enlightenment And a lot of minor fixes 0.63 -> 0.64 (Nov 01 2008) GENERAL: * sprites from /Sprites folder are handled correctly * several fixes for pathfinder and path arrows * better handling disposed/predefined heroes * heroes regain 1 mana point each turn * support for mistycisim and intelligence skills * hero hiring possible * added support for a number of hotkeys * it's not possible anymore to leave hero level-up window without selecting secondary skill * many minor improvements * Added some kind of simple chatting functionality through console. Implemented several WoG cheats equivalents: a) woggaladriel -> vcmiainur b) wogoliphaunt -> vcminoldor c) wogshadowfax -> vcminahar d) wogeyeofsauron -> vcmieagles e) wogisengard -> vcmiformenos f) wogsaruman -> vcmiistari g) wogpathofthedead -> vcmiangband h) woggandalfwhite -> vcmiglorfindel ADVENTURE INTERFACE: * clicking on a tile in advmap view when a path is shown will not only hide it but also calculate a new one * slowed map scrolling * blocked scrolling adventure map with mouse when left ctrl is pressed * blocked map scrolling when dialog window is opened * scholar will be accessible from the top TOWN INTERFACE: * partially done tavern window (only hero hiring functionality) BATTLES: * water elemental will really be treated as 2 hex creature * potential infinite loop in reverseCreature removed * better handling of battle cursor * fixed blocked shooter behavior * it's possible in battles to check remeaining HP of neutral stacks * partial support for Magic Arrow spell * fixed bug with dying unit * stack queue hotkey is now 'Q' * added shots limit 0.62 -> 0.63 (Oct 01 2008) GENERAL: * coloured console output, logging all info to txt files * it's possible to use other port than 3030 by passing it as an additional argument * removed some redundant warnings * partially done spellbook * Alt+F4 quits the game * some crashbugs was fixed * added handling of navigation, logistics, pathfinding, scouting end estates secondary skill * magical hero are given spellbook at the beginning * added initial secondary skills for heroes BATTLES: * very significant optimization of battles * battle summary window * fixed crashbug occurring sometimes on exiting battle * confirm window is shown before retreat * graphic stack queue in battle (shows when 'c' key is pressed) * it's possible to attack enemy hero * neutral monster army disappears when defeated * casualties among hero army and neutral creatures are saved * better animation handling in battles * directional attack in battles * mostly done battle options (although they're not saved) * added receiving exp (and leveling-up) after a won battle * added support for archery, offence and armourer secondary abilities * hero's primary skills accounted for damage dealt by creatures in battle TOWNS: * mostly done marketplace * fixed crashbug with battles on swamps and rough terrain * counterattacks * heroes can learn new spells in towns * working resource silo * fixed bug with the mage guild when no spells available * it's possible to build lighthouse HERO WINDOW: * setting army formation * tooltips for artifacts in backpack ADVENTURE INTERFACE: * fixed bug with disappearing head of a hero in adventure map * some objects are no longer accessible from the top * no tooltips for objects under FoW * events won't be shown * working Subterranean Gates, Monoliths * minimap shows all flaggable objects (towns, mines, etc.) * artifacts we pick up go to the appropriate slot (if free) 0.61 -> 0.62 (Sep 01 2008) General: * restructured to the server-client model * support for heroes placed in towns * upgrading creatures * working gaining levels for heroes (including dialog with skill selection) * added graphical cursor * showing creature amount in the creature info window * giving starting bonus Castles: * icon in infobox showing that there is hero in town garrison * fort/citadel/castle screen * taking last stack from the heroes army should be impossible (or at least harder) * fixed reading forbidden structures * randomizing spells in towns * viewing hero window in the town screen * possibility of moving hero into the garrison * mage guild screen * support for blacksmith * if hero doesn't have a spell book, he can buy one in a mage guild * it's possible to build glyph of fear in fortress * creatures placeholders work properly Adventure Interface: * hopefully fixed problems with wrong town defs (village/fort/capitol) Hero Window: * bugfix: splitting stacks works in hero window * removed bug causing significant increase of CPU consumption Battles: * shooting * removed some displaying problems * showing last group of frames in creature animation won't crash * added start moving and end moving animations * fixed moving two-hex creatures * showing/hiding graphic cursor * a part of using graphic cursor * slightly optimized showing of battle interface * animation of getting hit / death by shooting is displayed when it should be * improved pathfinding in battles, removed problems with displaying movement, adventure map interface won't be called during battles. * minor optimizations PreGame: * updates settings when selecting new map after changing sorting criteria * if sorting not by name, name will be used as a secondary criteria * when filter is applied a first available map is selected automatically * slider position updated after sorting in pregame Objects: * support for the Tree of knowledge * support for Campfires * added event message when picking artifact 0.6 -> 0.61 (Jun 15 2008) Improvements: * improved attacking in the battles * it's possible to kill hostile stack * animations won't go in the same phase * Better pathfinder * "%s" substitutions in Right-click information in town hall * windmill won't give wood * hover text for heroes * support for ZSoft-style PCX files in /Data * Splitting: when moving slider to the right so that 0 is left in old slot the army is moved * in the townlist in castle selected town will by placed on the 2nd place (not 3rd) * stack at the limit of unit's range can now be attacked * range of unit is now properly displayed * battle log is scrolled down when new event occurs * console is closed when application exits Bugfixes: * stack at the limit of unit's range can now be attacked * good background for the town hall screen in Stronghold * fixed typo in hall.txt * VCMI won't crash when r-click neutral stack during the battle * water won't blink behind shipyard in the Castle * fixed several memory leaks * properly displaying two-hex creatures in recruit/split/info window * corrupted map file won't cause crash on initializing main menu 0.59 -> 0.6 (Jun 1 2008) * partially done attacking in battles * screen isn't now refreshed while blitting creature info window * r-click creature info windows in battles * no more division by 0 in slider * "plural" reference names for Conflux creatures (starting armies of Conflux heroes should now be working) * fixed estate problems * fixed blinking mana vortex * grail increases creature growths * new pathfinder * several minor improvements 0.58 -> 0.59 (May 24 2008 - closed, test release) * fixed memory leak in battles * blitting creature animations to rects in the recruitment window * fixed wrong creatures def names * better battle pathfinder and unit reversing * improved slider ( #58 ) * fixed problems with horde buildings (won't block original dwellings) * giving primary skill when hero get level (but there is still no dialog) * if an upgraded creature is available it'll be shown as the first in a recruitment window * creature levels not messed in Fortress * war machines are added to the hero's inventory, not to the garrison * support for H3-style PCX graphics in Data/ * VCMI won't crash when is unable to initialize audio system * fixed displaying wrong town defs * improvements in recruitment window (slider won't allow to select more creatures than we can afford) * creature info window (only r-click) * callback for buttons/lists based on boost::function * a lot of minor improvements 0.55 -> 0.58 (Apr 20 2008 - closed, test release) Towns: * recruiting creatures * working creature growths (including castle and horde building influences) * towns give income * town hall screen * building buildings (requirements and cost are handled) * hints for structures * updating town infobox Garrisons: * merging stacks * splitting stacks Battles: * starting battles * displaying terrain, animations of heroes, units, grid, range of units, battle menu with console, amounts of units in stacks * leaving battle by pressing flee button * moving units in battles and displaying their ranges * defend command for units General: * a number of minor fixes and improvements 0.54 -> 0.55 (Feb 29 2008) * Sprites/ folder works for h3sprite.lod same as Data/ for h3bitmap.lod (but it's still experimental) * randomization quantity of creatures on the map * fix of Pandora's Box handling * reading disposed/predefined heroes * new command - "get txt" - VCMI will extract all .txt files from h3bitmap.lod to the Extracted_txts/ folder. * more detailed logs * reported problems with hero flags resolved * heroes cannot occupy the same tile * hints for most of creature generators * some minor stuff 0.53b -> 0.54 (Feb 23 2008 - first public release) * given hero is placed in the town entrance * some objects such as river delta won't be blitted "on" hero * tiles under FoW are inaccessible * giving random hero on RoE maps * improved protection against hero duplication * fixed starting values of primary abilities of random heroes on RoE/AB maps * right click popups with infoboxes for heroes/towns lists * new interface coloring (many thanks to GrayFace ;]) * fixed bug in object flag's coloring * added hints in town lists * eliminated square from city hints 0.53 - 0.53b (Feb 20 2008) * added giving default buildings in towns * town infobox won't crash on empty town 0.52 - 0.53 (Feb 18 2008): * hopefully the last bugfix of Pandora's Box * fixed blockmaps of generated heroes * disposed hero cannot be chosen in scenario settings (unless he is in prison) * fixed town randomization * fixed hero randomization * fixed displaying heroes in preGame * fixed selecting/deselecting artifact slots in hero window * much faster pathfinder * memory usage and load time significantly decreased * it's impossible to select empty artifact slot in hero window * fixed problem with FoW displaying on minimap on L-sized maps * fixed crashbug in hero list connected with heroes dismissing * mostly done town infobox * town daily income is properly calculated 0.51 - 0.52 (Feb 7 2008): * [feature] giving starting hero * [feature] VCMI will try to use files from /Data folder instead of those from h3bitmap.lod * [feature] picked artifacts are added to hero's backpack * [feature] possibility of choosing player to play * [bugfix] ZELP.TXT file *should* be handled correctly even it is non-english * [bugfix] fixed crashbug in reading defs with negativ left/right margins * [bugfix] improved randomization * [bugfix] pathfinder can't be cheated (what caused errors) 0.5 - 0.51 (Feb 3 2008): * close button properly closes (same does 'q' key) * two players can't have selected same hero * double click on "Show Available Scenarios" won't reset options * fixed possible crashbug in town/hero lists * fixed crashbug in initializing game caused by wrong prisons handling * fixed crashbug on reading hero's custom artifacts in RoE maps * fixed crashbug on reading custom Pandora's Box in RoE maps * fixed crashbug on reading blank Quest Guards * better console messages * map reading speed up (though it's still slow, especially on bigger maps) to 0.5 (Feb 2 2008 - first closed release): * Main menu and New game screens * Scenario selection, part of advanced options support * Partially done adventure map, town and hero interfaces * Moving hero * Interactions with several objects (mines, resources, mills, and others) vcmi-0.99+git20190113.f06c8a87/Global.h000066400000000000000000000537211342332007200165330ustar00rootroot00000000000000/* * Global.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once /* ---------------------------------------------------------------------------- */ /* Compiler detection */ /* ---------------------------------------------------------------------------- */ // Fixed width bool data type is important for serialization static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #ifdef __GNUC__ # define GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__ * 10 + __GNUC_PATCHLEVEL__) #endif #if !defined(__clang__) && defined(__GNUC__) && (GCC_VERSION < 470) # error VCMI requires at least gcc-4.7.2 for successful compilation or clang-3.1. Please update your compiler #endif #if defined(__GNUC__) && (GCC_VERSION == 470 || GCC_VERSION == 471) # error This GCC version has buggy std::array::at version and should not be used. Please update to 4.7.2 or later #endif /* ---------------------------------------------------------------------------- */ /* Suppress some compiler warnings */ /* ---------------------------------------------------------------------------- */ #ifdef _MSC_VER # pragma warning (disable : 4800 ) /* disable conversion to bool warning -- I think it's intended in all places */ #endif /* ---------------------------------------------------------------------------- */ /* System detection. */ /* ---------------------------------------------------------------------------- */ // Based on: http://sourceforge.net/p/predef/wiki/OperatingSystems/ // and on: http://stackoverflow.com/questions/5919996/how-to-detect-reliably-mac-os-x-ios-linux-windows-in-c-preprocessor // TODO?: Should be moved to vstd\os_detect.h (and then included by Global.h) #ifdef _WIN16 // Defined for 16-bit environments # error "16-bit Windows isn't supported" #elif defined(_WIN64) // Defined for 64-bit environments # define VCMI_WINDOWS # define VCMI_WINDOWS_64 #elif defined(_WIN32) // Defined for both 32-bit and 64-bit environments # define VCMI_WINDOWS # define VCMI_WINDOWS_32 #elif defined(_WIN32_WCE) # error "Windows CE isn't supported" #elif defined(__linux__) || defined(__gnu_linux__) || defined(linux) || defined(__linux) # define VCMI_UNIX # define VCMI_XDG # ifdef __ANDROID__ # define VCMI_ANDROID # endif #elif defined(__FreeBSD_kernel__) || defined(__FreeBSD__) # define VCMI_UNIX # define VCMI_XDG # define VCMI_FREEBSD #elif defined(__GNU__) || defined(__gnu_hurd__) || (defined(__MACH__) && !defined(__APPLE__)) # define VCMI_UNIX # define VCMI_XDG # define VCMI_HURD #elif defined(__APPLE__) && defined(__MACH__) # define VCMI_UNIX # define VCMI_APPLE # include "TargetConditionals.h" # if TARGET_IPHONE_SIMULATOR # define VCMI_IOS # define VCMI_IOS_SIM # elif TARGET_OS_IPHONE # define VCMI_IOS # elif TARGET_OS_MAC # define VCMI_MAC # else //# warning "Unknown Apple target."? # endif #else # error "VCMI supports only Windows, OSX, Linux and Android targets" #endif #ifdef VCMI_IOS # error "iOS system isn't yet supported." #endif // Each compiler uses own way to supress fall through warning. Try to find it. #ifdef __has_cpp_attribute # if __has_cpp_attribute(fallthrough) # define FALLTHROUGH [[fallthrough]]; # elif __has_cpp_attribute(gnu::fallthrough) # define FALLTHROUGH [[gnu::fallthrough]]; # elif __has_cpp_attribute(clang::fallthrough) # define FALLTHROUGH [[clang::fallthrough]]; # else # define FALLTHROUGH # endif #else # define FALLTHROUGH #endif /* ---------------------------------------------------------------------------- */ /* Commonly used C++, Boost headers */ /* ---------------------------------------------------------------------------- */ #ifdef VCMI_WINDOWS # define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers - delete this line if something is missing. # define NOMINMAX // Exclude min/max macros from . Use std::[min/max] from instead. # define _NO_W32_PSEUDO_MODIFIERS // Exclude more macros for compiling with MinGW on Linux. #endif #ifdef VCMI_ANDROID # define NO_STD_TOSTRING // android runtime (gnustl) currently doesn't support std::to_string, so we provide our impl in this case #endif // VCMI_ANDROID /* ---------------------------------------------------------------------------- */ /* A macro to force inlining some of our functions */ /* ---------------------------------------------------------------------------- */ // Compiler (at least MSVC) is not so smart here-> without that displaying is MUCH slower #ifdef _MSC_VER # define STRONG_INLINE __forceinline #elif __GNUC__ # define STRONG_INLINE inline __attribute__((always_inline)) #else # define STRONG_INLINE inline #endif #define TO_STRING_HELPER(x) #x #define TO_STRING(x) TO_STRING_HELPER(x) #define LINE_IN_FILE __FILE__ ":" TO_STRING(__LINE__) #define _USE_MATH_DEFINES #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //The only available version is 3, as of Boost 1.50 #include #define BOOST_FILESYSTEM_VERSION 3 #if BOOST_VERSION > 105000 # define BOOST_THREAD_VERSION 3 #endif #define BOOST_THREAD_DONT_PROVIDE_THREAD_DESTRUCTOR_CALLS_TERMINATE_IF_JOINABLE 1 #define BOOST_BIND_NO_PLACEHOLDERS #if defined(_MSC_VER) && (_MSC_VER == 1900 || _MSC_VER == 1910 || _MSC_VER == 1911) #define BOOST_NO_CXX11_VARIADIC_TEMPLATES //Variadic templates are buggy in VS2015 and VS2017, so turn this off to avoid compile errors #endif #if BOOST_VERSION >= 106600 #define BOOST_ASIO_ENABLE_OLD_SERVICES #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef VCMI_ANDROID #include #endif #include #include #include #include #include #include #include #include #include #include #ifndef M_PI # define M_PI 3.14159265358979323846 #endif /* ---------------------------------------------------------------------------- */ /* Usings */ /* ---------------------------------------------------------------------------- */ using namespace std::placeholders; namespace range = boost::range; /* ---------------------------------------------------------------------------- */ /* Typedefs */ /* ---------------------------------------------------------------------------- */ // Integral data types typedef uint64_t ui64; //unsigned int 64 bits (8 bytes) typedef uint32_t ui32; //unsigned int 32 bits (4 bytes) typedef uint16_t ui16; //unsigned int 16 bits (2 bytes) typedef uint8_t ui8; //unsigned int 8 bits (1 byte) typedef int64_t si64; //signed int 64 bits (8 bytes) typedef int32_t si32; //signed int 32 bits (4 bytes) typedef int16_t si16; //signed int 16 bits (2 bytes) typedef int8_t si8; //signed int 8 bits (1 byte) // Lock typedefs typedef boost::lock_guard TLockGuard; typedef boost::lock_guard TLockGuardRec; /* ---------------------------------------------------------------------------- */ /* Macros */ /* ---------------------------------------------------------------------------- */ // Import + Export macro declarations #ifdef VCMI_WINDOWS # ifdef __GNUC__ # define DLL_IMPORT __attribute__((dllimport)) # define DLL_EXPORT __attribute__((dllexport)) # else # define DLL_IMPORT __declspec(dllimport) # define DLL_EXPORT __declspec(dllexport) # endif # define ELF_VISIBILITY #else # ifdef __GNUC__ # define DLL_IMPORT __attribute__ ((visibility("default"))) # define DLL_EXPORT __attribute__ ((visibility("default"))) # define ELF_VISIBILITY __attribute__ ((visibility("default"))) # endif #endif #ifdef VCMI_DLL # define DLL_LINKAGE DLL_EXPORT #else # define DLL_LINKAGE DLL_IMPORT #endif #define THROW_FORMAT(message, formatting_elems) throw std::runtime_error(boost::str(boost::format(message) % formatting_elems)) // can be used for counting arrays template char (&_ArrayCountObj(const T (&)[N]))[N]; #define ARRAY_COUNT(arr) (sizeof(_ArrayCountObj(arr))) // should be used for variables that becomes unused in release builds (e.g. only used for assert checks) #define UNUSED(VAR) ((void)VAR) /* ---------------------------------------------------------------------------- */ /* VCMI standard library */ /* ---------------------------------------------------------------------------- */ #include void inline handleException() { try { throw; } catch(const std::exception & ex) { logGlobal->error(ex.what()); } catch(const std::string & ex) { logGlobal->error(ex); } catch(...) { logGlobal->error("Sorry, caught unknown exception type. No more info available."); } } namespace vstd { // combine hashes. Present in boost but not in std template inline void hash_combine(std::size_t& seed, const T& v) { std::hash hasher; seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); } //returns true if container c contains item i template bool contains(const Container & c, const Item &i) { return std::find(std::begin(c), std::end(c),i) != std::end(c); } //returns true if container c contains item i template bool contains_if(const Container & c, Pred p) { return std::find_if(std::begin(c), std::end(c), p) != std::end(c); } //returns true if map c contains item i template bool contains(const std::map & c, const Item2 &i) { return c.find(i)!=c.end(); } //returns true if unordered set c contains item i template bool contains(const std::unordered_set & c, const Item &i) { return c.find(i)!=c.end(); } template bool contains(const std::unordered_map & c, const Item2 &i) { return c.find(i)!=c.end(); } //returns position of first element in vector c equal to s, if there is no such element, -1 is returned template int find_pos(const Container & c, const T2 &s) { size_t i=0; for (auto iter = std::begin(c); iter != std::end(c); iter++, i++) if(*iter == s) return i; return -1; } //Func f tells if element matches template int find_pos_if(const Container & c, const Func &f) { auto ret = boost::range::find_if(c, f); if(ret != std::end(c)) return std::distance(std::begin(c), ret); return -1; } //returns iterator to the given element if present in container, end() if not template typename Container::iterator find(Container & c, const Item &i) { return std::find(c.begin(),c.end(),i); } //returns const iterator to the given element if present in container, end() if not template typename Container::const_iterator find(const Container & c, const Item &i) { return std::find(c.begin(),c.end(),i); } //returns first key that maps to given value if present, returns success via found if provided template Key findKey(const std::map & map, const T & value, bool * found = nullptr) { for(auto iter = map.cbegin(); iter != map.cend(); iter++) { if(iter->second == value) { if(found) *found = true; return iter->first; } } if(found) *found = false; return Key(); } //removes element i from container c, returns false if c does not contain i template typename Container::size_type operator-=(Container &c, const Item &i) { typename Container::iterator itr = find(c,i); if(itr == c.end()) return false; c.erase(itr); return true; } //assigns greater of (a, b) to a and returns maximum of (a, b) template t1 &amax(t1 &a, const t2 &b) { if(a >= b) return a; else { a = b; return a; } } //assigns smaller of (a, b) to a and returns minimum of (a, b) template t1 &amin(t1 &a, const t2 &b) { if(a <= b) return a; else { a = b; return a; } } //makes a to fit the range template t1 &abetween(t1 &a, const t2 &b, const t3 &c) { amax(a,b); amin(a,c); return a; } //checks if a is between b and c template bool isbetween(const t1 &value, const t2 &min, const t3 &max) { return value > min && value < max; } //checks if a is within b and c template bool iswithin(const t1 &value, const t2 &min, const t3 &max) { return value >= min && value <= max; } template struct assigner { public: t1 &op1; t2 op2; assigner(t1 &a1, const t2 & a2) :op1(a1), op2(a2) {} void operator()() { op1 = op2; } }; // Assigns value a2 to a1. The point of time of the real operation can be controlled // with the () operator. template assigner assigno(t1 &a1, const t2 &a2) { return assigner(a1,a2); } //deleted pointer and sets it to nullptr template void clear_pointer(T* &ptr) { delete ptr; ptr = nullptr; } #if _MSC_VER >= 1800 using std::make_unique; #else template std::unique_ptr make_unique() { return std::unique_ptr(new T()); } template std::unique_ptr make_unique(Arg1 &&arg1) { return std::unique_ptr(new T(std::forward(arg1))); } template std::unique_ptr make_unique(Arg1 &&arg1, Arg2 &&arg2) { return std::unique_ptr(new T(std::forward(arg1), std::forward(arg2))); } template std::unique_ptr make_unique(Arg1 &&arg1, Arg2 &&arg2, Arg3 &&arg3) { return std::unique_ptr(new T(std::forward(arg1), std::forward(arg2), std::forward(arg3))); } template std::unique_ptr make_unique(Arg1 &&arg1, Arg2 &&arg2, Arg3 &&arg3, Arg4 &&arg4) { return std::unique_ptr(new T(std::forward(arg1), std::forward(arg2), std::forward(arg3), std::forward(arg4))); } #endif template typename Container::const_reference circularAt(const Container &r, size_t index) { assert(r.size()); index %= r.size(); auto itr = std::begin(r); std::advance(itr, index); return *itr; } template void erase_if(Range &vec, Predicate pred) { vec.erase(boost::remove_if(vec, pred),vec.end()); } template void erase_if(std::set &setContainer, Predicate pred) { auto itr = setContainer.begin(); auto endItr = setContainer.end(); while(itr != endItr) { auto tmpItr = itr++; if(pred(*tmpItr)) setContainer.erase(tmpItr); } } //works for map and std::map, maybe something else template void erase_if(std::map &container, Predicate pred) { auto itr = container.begin(); auto endItr = container.end(); while(itr != endItr) { auto tmpItr = itr++; if(pred(*tmpItr)) container.erase(tmpItr); } } template OutputIterator copy_if(const InputRange &input, OutputIterator result, Predicate pred) { return std::copy_if(boost::const_begin(input), std::end(input), result, pred); } template std::insert_iterator set_inserter(Container &c) { return std::inserter(c, c.end()); } //Returns iterator to the element for which the value of ValueFunction is minimal template auto minElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) { /* Clang crashes when instantiating this function template and having PCH compilation enabled. * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype * directly for both function parameters. */ return boost::min_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool { return vf(lhs) < vf(rhs); }); } //Returns iterator to the element for which the value of ValueFunction is maximal template auto maxElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) { /* Clang crashes when instantiating this function template and having PCH compilation enabled. * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype * directly for both function parameters. */ return boost::max_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool { return vf(lhs) < vf(rhs); }); } template void advance(T &obj, int change) { obj = (T)(((int)obj) + change); } template typename Container::value_type backOrNull(const Container &c) //returns last element of container or nullptr if it is empty (to be used with containers of pointers) { if(c.size()) return c.back(); else return typename Container::value_type(); } template typename Container::value_type frontOrNull(const Container &c) //returns first element of container or nullptr if it is empty (to be used with containers of pointers) { if(c.size()) return c.front(); else return nullptr; } template bool isValidIndex(const Container &c, Index i) { return i >= 0 && i < c.size(); } template boost::optional tryAt(const Container &c, Index i) { if(isValidIndex(c, i)) { auto itr = c.begin(); std::advance(itr, i); return *itr; } return boost::none; } template static boost::optional tryFindIf(const Container &r, const Pred &t) { auto pos = range::find_if(r, t); if(pos == boost::end(r)) return boost::none; else return *pos; } template typename Container::const_reference atOrDefault(const Container &r, size_t index, const typename Container::const_reference &defaultValue) { if(index < r.size()) return r[index]; return defaultValue; } template bool erase_if_present(Container &c, const Item &item) { auto i = std::find(c.begin(), c.end(), item); if (i != c.end()) { c.erase(i); return true; } return false; } template bool erase_if_present(std::map & c, const Item2 &item) { auto i = c.find(item); if (i != c.end()) { c.erase(i); return true; } return false; } template void erase(Container &c, Pred pred) { c.erase(boost::remove_if(c, pred), c.end()); } template void removeDuplicates(std::vector &vec) { boost::sort(vec); vec.erase(std::unique(vec.begin(), vec.end()), vec.end()); } template void concatenate(std::vector &dest, const std::vector &src) { dest.reserve(dest.size() + src.size()); dest.insert(dest.end(), src.begin(), src.end()); } template std::vector intersection(std::vector &v1, std::vector &v2) { std::vector v3; std::sort(v1.begin(), v1.end()); std::sort(v2.begin(), v2.end()); std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(v3)); return v3; } template bool containsMapping(const std::multimap & map, const std::pair & mapping) { auto range = map.equal_range(mapping.first); for(auto contained = range.first; contained != range.second; contained++) { if(mapping.second == contained->second) return true; } return false; } using boost::math::round; } using vstd::operator-=; using vstd::make_unique; #ifdef NO_STD_TOSTRING namespace std { template inline std::string to_string(const T& value) { std::ostringstream ss; ss << value; return ss.str(); } } #endif // NO_STD_TOSTRING vcmi-0.99+git20190113.f06c8a87/Mods/000077500000000000000000000000001342332007200160545ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/000077500000000000000000000000001342332007200170125ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/000077500000000000000000000000001342332007200176635ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/StackQueueLarge.png000066400000000000000000000044071342332007200234230ustar00rootroot00000000000000PNG  IHDRLRɕPLTEd2) , . 0 1 3 4 6 89 ;!<">#@$A$C%D&F'H(I)K*L+N,P,Q-S.T/V0X1Y2[3\4^5`6a6c7d8f9h:i;W@-k<l=n>p?]C/q?^D/s@`E0tAbF0cG1fH1gI1hI1jJ1kK2lL2mM3oM3pM4qN4sO4tP5uQ5wQ5xR6yS6zT6|U6~V7V7X8Y8dKgQhQjQjRkRlRlRmRmSnSoToToTpToPqTqUrUrUsVtVtVuVvWvWwWyXfiij}x@&O(U,Z/c2i 3m 4m(8e 4n!5q"6q"6s"7s"7t#8w#9w#9y$:y$:{$;}%<&='?'@2Br(A7J*E/KI[lzs pHYs  tIME:2IDATXíX=k8}.AB6l3Ca²I3R ]ƍ]bϽ1ds_η_x}=h B]yn`oml+b]b*;5!kU(S4`Zb0Wzbfzy`:cyuMQ;9TuHM)/UMΚB`oP367 RJu#3^=6Z}xo_$UU`wǚλBIayWorCf1[jTo> 1V潫jTWuy" 穞,}8 |wcfZY/eIE}wZ^v p0uUWCb Z bܜgW0$1F.$GF}fC|%1rPkGG6 ]LDFCku({I zs@ll^5_[Df3}S#IE%h<,h j"ʆ$ϓF QNO{S*m^.JE #fLdb{3O"fK$^# *$gG ņHt$hKk O!TyBNKp@QnZT E̪ȪpYC5G}  խݡVDVVТ8G`[HeoCܴ>L{~FI=!7fl=B[G`VqMb Bs"u~گ/bhT&||CwI⾉{Aq,#{ j/xpA>0{ ChEf8:QG!4 Dl%yy}>B4}?Rw8<KwiiS8W҄e'<Jck2䙡2=4WF,Z9ΐi&`ЧGq+ʐagrNXD ['OĐaG:cA=NqXO a1 6c+ؙV+HfÉ OGEl[f>YILf$#@$A$C%D&F'H(I)K*L+N,P,Q-S.T/V0X1Y2[3\4^5`6a6c7d8f9h:i;W@-k<l=n>p?]C/q?^D/s@`E0tAbF0cG1fH1gI1hI1jJ1kK2lL2mM3oM3pM4qN4sO4tP5uQ5wQ5xR6yS6zT6|U6~V7V7X8Y8dKgQhQjQjRkRlRlRmRmSnSoToToTpToPqTqUrUrUsVtVtVuVvWvWwWyXfiij}x@&O(U,Z/c2i 3m 4m(8e 4n!5q"6q"6s"7s"7t#8w#9w#9y$:y$:{$;}%<&='?'@2Br(A7J*E/KI[lzsbKGDH pHYs  tIME JpIIDATHǍUAkFk0`t U) !!lٽS|mwq݃"'қ7{ᄒyn\ֿ߯VXz^Wg[,_V|6og׳ܹl~@)ғ@T)Pi9V)Wo9Z_spSo+ՖlaM.:c.8qu75XNO]!*uC6[8u\!Sp3$1)o{qd%1zxvāef8ՀYOts'3\{ J ۂ67 @Zq"&OWjGa*$T-zmR`{~ 3fCH5Бi (@gthCxåU Ĩdh+WFD񵄉cU5u/x]B6wcd$$&9JJM/Z/pY*B$(\2̜W lviL+XO%)( Usj\4M ^Os.[*KdŐg".Q5),`q#% IBAO(I#+ [IQj/;kvEud^AU 60_M@ dʼ;&חJjR̺l)L unF(MaĪS}p-q!jq [&.""('ފNqx4bh8[G0=t9NaE^qx;Jc-_\!^&-r4w`da%ASK`tr  C%o{o i? ~a5vt]ns@{pww=ɆNfx}?v ]7< {6n^YaϧnK08Ʊ:]n@ !~ǽ,<Þn+=(jtɆT DkYp!WHEYn~|Џ0p ʯNۉS+oh3~U'=|IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/questDialog.png000066400000000000000000011754331342332007200226700ustar00rootroot00000000000000PNG  IHDRdbKGD pHYs   IDATxɮ5kv-4*"V8ML|!Dѣ +h"tЕŖ!yW_Ec9#?^{/f1c:B sCm!` w<p=rhsÔ@÷{\wh;):*;e\~8'̹>#ɣ4CXKCmoaQ?yguG ցR;? kmHޡ ?WE8lykk[f]KhV|1ux>DbSH5\0) [nH:WywGmux84tO;S{3%y16/u`. :,Kps^!yv~OsV&5y>D\֊V1"߽,c@p['1؟wkmXJԎ18VT̹4$ux+rk?FP;[ZaSׂ!"yr^ߧ$*x=*φ>')tN>>EwSc@r?(4z|,1e.9m<J;gz+nkE!<.)yg1/ץw9S8~.ryy}1"Em@c\jĄnϞ>vYZK-7Yǔy'R:y?_x]28PjGlhq~fq$V.g_cqݥ6\ cc 11'x'Bυ?;z_9˭s7:bT7%)=Jg0.V/kX.Ay30XѺƄ֎c GC8x8}~ct8r)C~K"NA䢷.w t;b&]~\Vy}2"odpcq$Lpe"kq›B<y/ i`A<=7ǀ)bJgISyWt"7z)u<nkC?9GKUu&׎LjUyv-e耼g?9?S!y',$YG)y,c SX޹4k"tmо.NɞCS yJJHF&庎+Mb5=kgJvZ;cSekXZǽ48'I^Iл IsC򙼗,(jܚu<,CpKG1O~ 0`=0E_{ $[\)aXKd%8+{B+Ϣ@~RtL2_F6R;,aRl,Ɯp sn^ Z-ƀc딢1yܳ> gy>4T] '6{OG b #_=|!IҊs.y;򝁵II G¿?~; h]jRұ^׊RC$vzoݺ B"mI ,j"{ilHIX0g{J j) Yq,.z+F\ "uX"ݑnAHV2Y*bQ@=":$ QjLJCdw*ga^Ȁͺ@;r:0o*E09. rzo,hDA~fAoxo*]&Q0ϙZAnKhoȗ! d%q{Azr-~\Mk;1@qdAV^p >1xZ@Sx">= ?h\A}:%|:% uXD"$vasQY1Dg4 ;C0)@d|fyMc|_&86Nz-9cx%ܧCDgOV ݚt{>5^&Q!9sߩve),)xYܿxxHD"E&%:W*͡6]CA!HεkB=7&N ~8v|ZuNRE5n) y/xW6]%Nt6cY:?w@-VB{s˳:OKHiJQ݃o7?ܷ_5B^/D6(Aݝ݅B CzZ{Y<^`iAJxۇ1q (trx&@tAP.$ Brw6 K5(z) JC0t\D_@jcaw2WKUڕMN+zyvm :u[zՎvk3ށ G8SIvKn2bҙ8^Tv |-C~鮃wP'E2@X;[i<Ԃ^h}Uy˛R{y/{h-tuiĶT /Kq@J?6ֈsjAmp.J2ΚlVO$U0DRC$rY8zfAgrHo]&{8$ϑC OA/S@k 2*y$5ԎJG(vҍ5s48yeMR\ܳdtE:Bڱ~2?N"n\]^@,c?I5`(Ɣ낗](bA\Kxp`R*mZ@;M @#襍Ϛ$]R<>N$#o97uKTy_Cxkx+ld[7$U㢾umx>D9ܖ२9wm Y.zilg;p(R5c!R0ѳ6cw>:~F9N2> z>>*V;E=q?0o~=nLT cC a9lir0u>Fg#㝢ljMgt-hwvɐnn A }Һ%)yi\RaPc<y-*:Dgc=HXZ2r6k9`̥m@C@Zd443hXqIG=MQ6'N VC5 |NV(CPҍWM:)X$I/K8[ AEoG)x").Fvq/Mr^炥6W)leT+x5v{.㞥({)@<Kp\lz] GDG-z-c>u2~g3IH ށ9,͒!ncȹ5#}:%L)15G$9&lZ󫜌v+"xc΂F/n1w2uuDYiSp~Y*Hemv6O(:i?sM! Z3=1Drm؁Gxځ+Mn2l+UAհѝ+G3uh8C::0Z^%36©J|a1ψq:֜x_׊9sp|zWW Qڙ0ٿܲ}%Q.`!v\cT M19"17x1}gca:嚇(qB?8"lT8Ksh ߳4~ѻaw߉L)"g`h׺m{uooIm@¿☂N+ϽvZ H\ u6ܮIa¿`q~]et#1&ϧdcjEz\4oOI;"OH.;#ҠT)`cֶ16`jg !L#^-B܍Ӧi 'ztIܜ9r;xN+y/83Pc?hc [B0EqVt( (petfb!lZq"Ő'VX|q&7~KpLp؊(`,Α&J^ -ԑߜ1Nr:WGFcyu uJRxk=.w"ԂP.BIp5z"{w~AHoy+C`)$1.85S ${ J>ndKxNg!X`^p,XgP9D5k> 1-z']eᔌpmC^9r jZ&0qCV;ξ!v&YA`'JKU# $]*B֞Ut@u􂖕&Dvϱ>=AFm™j֩hWGWSFVgq=3:[=w* c;$esz+r^Oda)GFKCu8 ^Lɷ 8/,B5xQ=ǎI;d0$t/%Y pխ%/LbW*wT^ -nk{A"j<mPC#/+ǂJ]G8NhA~Tk |DԷr0æG >T-f6SqyKc"߉ݰv{™A88- J+UDF-X$Qi7`e8½J$'M\i ٹ( ctN"AhEί$Q鴦PwbE]I 6Dg*; ҡmAA d 5E~t,7%IW)g yɚOtۇ/ g!*SFM}z#nJ1H}2`3?P{ץ-['5r[ۧ)"zO*2q,i١=8HAř)2ƁS:ґ?G*eKE20K!uAmRr>",-IQ@$tyM:acQMC:Eܬxk\:{Z|LN{)3DJŁ x|GHqrH:lx}2LϮ@TY2~/kcKފ0EVVR|wr6ײ c Ϫ{rU^cO䙮Ʊei~; :zqc SE ,*VT) K~8%FL+rtTHX_G߉4q50' HRd8ʳuRoaւu<%҂3(vhAA9 \qnS%+£H[# 'NGLq.kC־Yc41I'-7KZ 'D 3?r{!֛Rh) AHSXrj ZHe؟vubYh^Q*eF<JTE#䧲!xb ebm M >+<^|VyF70ghpHPMM)~k7W K@Z7Ym]J砑CH›%hc%DZ.se(]IĬU^*OT*v|>-ėN}]0{?yYUSo*M^@DE$س;H|SR uN=WzQ[=/G(&\~f5Xv܋"x&7ELp$Z {tN|y뜠 ]ʘ@ mo9;|TI {BZL߿f` 悙~];k"C0$@G+4()R18$SZxՃ_|+&tO:~pY>媘*IUFp'J${$Ky>yvŚhSn\μ8s/+O=gQ+GFO)wPoDj*MG{Ơ;|UIHmHJR#Z?DZ_(lPU3{0et&RP1Kƍ虐Yqip%?*?T|7dp`_x /DΟxeXmLs:;^#D"mg|^<~MQ{}nDXA>vm)c3SjNB^ԼY x; cHwH沾3n4U+8h̥U2~)_PLf ,F[ LE@#Ƥ4aU/klݧk;6|Rpl6[tlhB ,U"\)\)E{oZɯě@ 踶I{Lg7p$,cduQ/s:\]/Vl]`*G1 ؙs{CV.۸:xj_3xyֆ?-&nobQWw#c?Yxa@V([)9 QPo5vutF;55$JҠ1&튗M X,*RtlRY^t \u)H`^U3GۊU"+ՔJ{cAB~d(􍛙|ԇڶ "Y.OYM^_t^![Q޻%zC*!zX)S*On퓢 K% aSo;Ơ_FFQBN!1{)<.i88yr]Eq]Yµ2 #gַv1=3bYullߌJ݊UسөSG];FU+0U|*\cyօ5lvitSq \0ЃN?y.yWjw&&Iϲ*T,/tO?ߊ;꨿e ]Js)wm_?6snA3<2(8`_C^#O[7(>[TA[;ց߿!S}\봩V5\$pݔ=s3P:HŐ] iWM":r-^b׌*RՑ{}Ufv_oVEm 潳h欞.k} ݿ6([HR>,<ײ#6*)Eq.Zȩ ;5u7"c#4HB-Zpw_·Ibkv0E<J 軵3O wz3K<$Oځ`ř1ZhvnA;Zp*Xڶ+{X"lCpf< c/\QBa^ۚ%M]+~u:j3Қi,R8[qwEr21Ɗ*BѕgEQ$+ﷃQChhWbE1EokE&cβAm;Z RqHW65lP;(] W9|g[ϧE!y\w[Y6ŭ{s:PɱmPm-n7SdHi6_o3$cd3+X-k'3U>*Fm ڵv|qx7/)yvtknFry ߳oO$Jycp_lwq^b蘪v &tB)m6MK3 Zu}&} nmS.-g 8du\mV$B1dw K5/'5,Bna`z+sMQ+C0$hDHK *sBaA1u9[7m@\#J42%`2hk(_Ŀ+{x)ZӚ|U.} C0 5Q9HA&o?]gd.)̙2١%лm&A<ߥ6<"YJ_8qTld_:,8_o1x|"}Esc꛿ 7??{VQIbT+?w#v B,svtJlo_f+]ыE犈E[-S)_OFb8uD3:~Q 9^ km5u*L8 1e] $$Z"}]y'q˂ԩcb _rŧߢDž:l]@蘪^L{+V(.#!KыᇳlbfPdD@s] (m ӽʇ(gȖ;\j7q~mKx]M%\rYНzM{!t R˳$/ r:>`(MǢZx>72ݑ(^IEV r¼6"+D] BhͶ[fXc!9&ԎQ<.K@uwQcPmsĒHQ9V'n#F7[;)zߨ-oVp]^l΂΃GtNd("q݋m'~aZP͵txWmm-lCuD މ ƽ`@ksUJb8UZB.5 (Pnvc{fIts1d{&uǀw ^^݌W1з{!G7L䝐%TO miލuB0[1 6R!NEl#]qxkqy6e庩dhhǽQ?ZMd`sz篴VS{[[?. SrnDvxi ,1%G&dŏVޣ)لRmi`yu{D~yNNE- 3G_׆||TJG 7 QB'mRt}n oq-'g-jF`;{NXjm+56՚M֡ɞbgcT" d^ZrVH#z'uWChS5|m @&Im9-)̋KR;&ZEkF!,,4D! ,Exz@0nZdҺDkc-5Δ]G=0,KxwJ؄O0sv~!:0 %7.lc}_;)\5"]voaRlyhpYn+K*iDNNLܼ/s!.I& *x/SmuxY佖4- -,FLښ&vkZA[KC %!El;f6{aJb{) R{;@#B ߎHoEbS3R1G9xHfPh`K,ݎ!F|6jԑ`ӓ\8F¢,uh]/K-U剪k 浡jػ3g-mN䊹)E~5.*:ʕU:ؼ2WInkTV< VHyWU |JqGm "BWe˒kCsX[„](ƛK:KDvd{-P|Y+.sGu5eh~.b:UYбZw2_ТrTvAf"qm-]]fox.Esx>$t4S8nGa;yGtTꂮ5+M(qHa-|SsXr+ OH& ˜ˌ?y1%CĔ%=@G*Q%˙v $(nCN'ԅp>-8] vܱN=Sei2r2ì<|5?CHyCZ|NP;G.W#X]ǜaȓ'Om&;vAumU yJu'X Hl(`-ȴ`y$*Wl V#ϥ<&z}n6D DL:%8Za婖!CW;?oS #E7 8Re&b57 _qw<>/|ȯYVpss!(rB^>K1x\8$s hڊ{8NђBIŅ{(W/#?yH(Ub;t$Ե*3Ք:΂wu`ɲ[mS@iW^jZL7ȁ(ɕbuQwsngtКNlkm(,$zE |J|CH@Epoў<.x>~2~݃v#$k Z`њ!D!7ws[yxy^C0WU#[He]*t W"MϤNTĝvyE]uH$-R )`{nsk˂/ׂ|EՆmoɡs +BѸ/Քsټ' ֆV(0% =ig!9U]357*Iq2N4wS@NUE :uoឋMJۤ kzљ2uΛzGQƃiD6~[VŪnH-\Jb3Ba:+ܗ OO j |vNq5=ӛHEp}b6ȻQ[s6F <Jv_WXPeQ ֧0stH^&_U48Kt^V|8t#V]kVH1q$8]j-ph[|Z}MͤhR+U'GIk1Zs)zmj-T) ǹ {:|DD5u=䆕kBf9sW;"۳!DŽ-vrނo LH eܯE FW YQcd 4iq|(yAa!r]WZ\GboJkCQjИj3mBp U7D\Pa1:5ruST5lƱ]x01)yDU|^Pg?Yƕ]v%r,\m͋ҞRCҝ>^f 2?]2'o怯K|Am& a*^;DeVd?]IQIsi&K sxWPXQ=|t@0cڙfaϜ!:AGH+8n=S>XR;4tbp՛alڍ?spRQA*x-ϵ4gX(jqм7TLW#])R؂Ą~Ăa- WpL~͜W#+9+WoȘ5 Q4%WeY< B|W%.K4W%+m7$x\3;h)q Ol[IVgp&غBwa}pKzHZ/: # +y7kip:I/h˲I5#J "- ν_ׯZ^ĝ9|ezD簒u%+͑sWmQ`+MԽzʡ)GC x>Ł㻕*$Z .Q Z;mպRZAuse%Z&{tW_?j9ЫiD~[-WeXkC-g!+vddͥZ!.݈2^rf(H5јw`E.ag<s]TS$q{7CtWUbox 9WYH}\=CKn]=|Y7/,5#U@:tocVdW]\Z솘T;+ETq[ (pXܙHn-Ư{IRK)YIW.uJL}qC^>'?Bض1t'YJC.H33\lD St6Z*Q!f԰uVL) l0'ǴyIQVY>/ÿ|?;>mcaTZǑp}"YlsHA=*܊!_. DiDrKwhNxo4{\!.#]s%Z2F HL(н"8;-$WsqɼKK똜Gڣ#:V%iGN EV&h]OY OE x}޵iL6SL~fĆ0^GŎq ,OιҰo\\pwv 6j_TAwH/Jq;0m,/D\W”:*w+V6~G ]7mU2Lc5Wm4{z{T AOq.E=X|sn4*'nƠ:$2*/ehcG74J^l.jT=Ng٭a"_G(P =:eҟۭ4:ob#ZWI~Fd▟C6rtiCqZӶ1'oEnuH\+.AתVl]镦ϭC6AX2wB6Ee-7DgVg^~Q_9)DhLVa)[˻,> ~<4tY1e~PS IL ;l{?]*]saج:AGw i\cE["y@96A}6ThxY!Q=˘@Uxac`Q I}]*`?Xh ѡ5A̔< C;RsD[v mrY;}^Ou(DUdCt8{,]yUMmf2E{$4սspqYDV\/c301)!:vq" QP-VϙHԾshR9W^@t,*P-tJwg.lVn^c- nqj4DFGWUҶOј n "nISzgP `KPsق1- cZ/#i1Ն[Fp1(*2R04:0%yF=V1a[Yդfk9\jm5%: |ёtid0эyN;T彘v&f筠LY9Zrܜsbg2ow;)m&n@4lDPQ i/ Xׂk坨2t$D]"(۲y)GF9Z /rA]#ZEC'Kxe\tz]9 ct52f`QՃ8F~-6 5ˍaomχM T:P2u;ǞKiptPZBQ`6qHt1:TFUDclƂ\ŏArR\+ }}Զ!`km?ץa.R+Nʪfޢ/奋+3ӾS$Yux1-w87sISpNdb)h4pC_=~q߿Bb $Iq-]3[1G#vg7diI.;@ﲧrҥQZ: @7R`mU\ѧʱs9w Ry-"9YЗڰR1d$4D Vmk#p@oZbk1o 9 f8y&)ƀowA $w{r8Xq¶ۭbL H<>pdֆ&%AupxpAwI#Υn:LN+`['<i^6^ʳks/1筙ݑ;.K3Eo讬V+TGذDz' G_3(tT[(Oev##U)>^fp |+8,8JӂpF@Ir_rD(R} ҪRhq0rܻv]\6gnxxkw0I)^/7hˆ.N%cl󡚲r^#X<.uZ3bz|Y?Gx,f-X|ZLn?+2Bp9G<2fn̪ y1 MA8zssv>KFp!0w;̖>=` N&5t^ޚjWmkס"-6%U/.@i1V3{$m͐S[XkТscm9`^niWjG s g춍uo=ƊpқNgvˁjEYDQ'LlR?w맻Jo:N5+pk1I1zd+ 'j{DۑɾI.Z*G^wm *"]>k0ڴ1kTָy'jLnF:Je-uAtdK`܋l|:|F m˳y#n[u^9p6鯋ܯ̼fJ9SGݳ{5}6̈́^<*B>:r/d KeDuZuN#o[sE3lݽE|]m(}YM z#jq0ܠ17LL(lQl|h@cEBga"1ۊm2}Ycxn rAx܉lƻ;{ep4踖bĂY`,0>WLl Q6: CQuۚ(mSaTIwITibi0#1#I̠[N>uKs5(^Jqf`]X`BkBTÙg19E̴Nt"oJtx[wr0Ym|:%qbҌ&1 B@VFrӴN+BG] Q=nqMv..@:ʮݝ})F+-)0L۽x"h;ۆS%gvG[uxv4PX"P2t(H'W*YgJ2L#M&Ȏƻ*\;_a/I e 6 -tk?_Nd#Z?Z0"rީypIF6J !xėF/k.V}ąmlLx:FL_&ţz7>5E!?{_i;='-M[x[w[5o c-z^Yt̀j|L@5:?.?O(JFQJO$`]uoUs=P[A`W̻n2%\4DS]-EE~o Te FNwl ȳm6j/զ8(7kLՆKD=)2jl8b v-$ߛ}3x[cfx!1׵9Q" GjbcݓE2 0wi@dHnL6*TVdGr:Z>=rmkL =/SX ^.RgF@1Hw SVe !w׳^XPu<"^;fzъG4܋F fUuJZZ|٘ml6 ~uy4d ~^Ɨ IDATKV";G_s7vI4}/H\ZB;mBmê8;s HH9yI6rk1`̞éE^;VE _K^03R]n̓n8n>^LNր;JaN"j@1y@ l8Ċ_)[f 7)f@7R;s5 7^DirӍ]^++7ħ0ݝn9|×sr/gZ 0+ {m[V,8/ޟO!͸67|&ZgS9F|9'JGV4mŲ3 jϝ*' 77Gnm-Ѷu D3,9yt oeZʕΓ$uh|~GDzxKYQc/l fòҿ`@T(+u!\ZU+cԟ9BL܍c\x$J9,w-pMhK.81^Ͱ:;%G~AW[ +m)|Yiu)]{qᄌ^_9}wx˨Ћ@U A.s0۽<Ë0igVFplB^qfMuԜ"{E H1]w !#8M)2evѢwR6}r% ?7t?H-Ԕ7[::)ջb"v`ysuWF#V1iӅ dۆ̞{Xw(Uy'i~x`}HlO.ʎʏ2h^nnϬw PNߜne6<[~ z.O9+[񸅋 G2KTyvg"0g:sjxP:"k֛3o̳aJx^"^j%"9I8 YC,M?>A):*0 :kK0,6,}h:}\P"YRD\9[gxxs[h$NUĂRXFt@eg<'bTE-ar})I~ߚ/ cJ>z*Oҽ9T 5wf|+6cxNdgZc *Zo$IV.PD?бÝ:ASܟ|Q~gر\Z4A߸с#-p8)=^5C Nnة') 2_+γm8w&Y9mm 99~mx[ yF&S4`XFdVGHWbojSһc$~e+ qꬾnW8oBsq(qpdL[gOHYW7sXWFFf#z+N }b|\ˆicZpb\TU0VU8_X Zq Pj _IaOnZLnuqu3VA=R Ζ0|!b+u(HmM`<,Stºa“;s-.(`,BhlulI?7qV ς̼ɚBt}+ Up2g/]Ѐ(SŐM`{)}=btrd.:krGJPBbIs)ᙶʀѣ>~ S\運ڝFk A׽n%'bGDjLQՅ~@UAyps$`ӆi:1>1*ztV9Q`'J28RuRȥl 0NҷN˩G[h4Q)ޜlԻL-~_[VHT$"~CU]q[[E_'݋k#k3=U aߣ$ʤL >]\w{uކt'ҐYµ-ܰ1s%nbEn@N`$73Dx4PһB$u̽dr *HmWdWw= Mчh#""˜RwgF% md)q&,aMj k obLnl(#k1RTPƘjw,)YlMHL"{B>2i:jvB93+i1fDJ뼊͵?NmG:G72YX @ldJ#ϺUڣt %c[wݛsJ(a! Xmhq4 t7f4Vm˜,C'#L)r"LKhL9*ӎR`C u2oFxpӧSgQPsTP}B+Nv{7ܻux`鎦qGmoÜ௱cu6/e9N"=06F^VJy&ۆ/7>)^[i1kә09_˂:f%|)OG)è ]H1^ ̿, 1hes]`[C݂}+?qhb݉}'ŠvqjatVԺa߮ c7Wup̈́\GϝmiÀນFYcGtx͔;tfPc$Ձ1?\qL|̬wdmļܺuD8oH'TA)]hR^̼:T|vD{̵Q1cMc +L7{ȿ\&w Zi!?OK]Ĕ4ۭ̮nE/Z[S 6r|uᙛ[>fYjt'434:4L=JJF? "ʏ,GV!no@{i9 z6NO5#B̾_evv7VtI% YN)~6|ߪZ˖.Vݭ^\&e.2Xp]6zН`BMcy ; 4'| Ḣ=ϑ]edEcՅiW\ٹoR 7n{mkԑnE4֭m]rzuWd0Ĕ{cYBf^Q}3oűQ26%X?{_cC4Աv"1Oɞ3SKJhKx6_WXwN2:PNZYi+ S,[u'b[MXVE&&5G~^?340=uoNx/ۇfD}}YrFۜΐ0Z3Z︰mts ( Pb{SL b>M1đUv0:e%[l\WC6 6#ZN%YE#bEf#rߠQ/F7RKw.f3XieQW }"nN=Ӓ)WL ;NSw5J'3Uݛ˒♄fŸ׷e"8P?/xi8` \oJ`sV+!? * ׃ȮuXw 7N?ւWi砎(%[tBl|J1c0YE>7>4ӧ6ThzVDccR@bp<"-jk^vuGj c@^052^Ezٚr!u; dr2yؘExt]q:SDʹXRDm8-<锌B-UNC7Y^9i ߮m NϩFPsiZamO wx:+a.S,&k%ۏ.͟0D*fԱ;+[i6 EBܸc`+35\ҁl# [x[ZЩKE(*։m:7;M+f{4e4YT;G&!um(:9(; ݛCVw3ݟƿ'$ pR"9h%R+Qml>H!"cֽͣx#| ?Σ8_"DB}_澿x٥*θ6"-TUw;(p:svluh6־..Ǔ}ϪKƟ(ֻRD+VX?-u7ÈK׽ao# Õ 70=wuw'[id7y}^?U{?vS·ڀ;ydkij:vm^|sO| :ÅxȪXK @FZrӉ8fzijehr1kы)Eio54"x ]=YR߷4]uY^fcBnuP2+NcEө\f@t.*NxzvyKBA|P`pcqxUht)IWOq#~d}ضGGȨ`z+rļs|,<|<4y-[bP:(* )uq,2ewR9{ib?;h\{R`Z9X6&5}[{e2,fs2 {\2Evr Vc700tirylxT< 02mY?HrfLhFq>꾎8 =аpz/{]JX0s{ڷ;Ůen]*G.*j v:YxYk-o82'm'~`n_ꤨq%bQZg)s6ϯDY&<тwzi0>2ۭvou (-.˖8(kp<Cd2FDځ^}f bU+ht:oiTxs0Ns XRDei&I\/.`v9 Duk8Ov厓vx) D3WNS& ir*sL8Adq,(9?4`Ȁ 7v'syн@So"qaЙr01mt~-z2W o˩ByI.6WgYV_:Uز1Z DD-6~5Ǝ1+DYѯniٻ9ȤӾC#-ИqN_qXUfv~a:-ɺr5|Lpz/vѓRW A{`^)7&\\TdaXIл ]kcA֕T4HCʑiqpS:~ jknTX,n4h!qNy'f  ?O#.0"$-c5xnfX &vd˔eg|=g>Ī,ydX4CZhhe&bZU@<3UIT4$27Nbwpƃ~hC2eqZ/o[)aoԍ:H Ht} 1 E&n u;tjS!f۔[WOR60^k?F$j]jH_=˨09c#Y8CK)tjV| a:.q˞}Qeg/X( OvWtK@tsr+0VLL%HcJǔ{%Ll}q8bH fn~PcMEbخjw~Y#^2M8o9L_h*l{' IDATѳ49Gl,52l|nU+ޥ)5 bu<.6'鬶n7v Z7Rb8zX9a] ߿so SGG2G=}Zø7+^:lЁWc]ApUeiDv̅ 5gT;Dݗ9D~x)\C<,NeP*]ph޼at3쐸Ն猅w 5kGJ㜰ʰބzj>KN%@nU/mtH^9;7hEѡI,̶EZ0g~}6E ?}='܋u!ω?=p2%x^2kְ5ۀ)}^N˝c\Yn8;,=ܤ0i];„GzԀ3c< S"4Dn49mEf ;-jȇMTtdԹ;<,jcNfݾ:u#rjZm97{#slbי1N}SjNF阀Jv˺2a-jc&=#PPc*$!D2Y04:?֊WGGe[Nij# 75}/CչUu);CL4Xy `h-v@ԝ̽)G޹Iz?Nޟ{uDCBG]^=i29'Xa"fi's'NN%,[X"1bo~C#պky=#+Bn*Ǚ5a0.9Zi~dݛ\h ty L|yWX.SD^nځtJxWy-]+]̓׎zSĚGLgwPՋmI#A)9mJk5:^}rN~2SH`-׽2V)L7ɴO>ZI>B'v2%3*73.c ((FF8tr x7kE-^W:c{JG %E"O,mc28ÁcAth'hȳN| ,]^TVpisPsQou1䠑F1aL*qw.& t8:Sj~ث2e`XyAEʙ uy8* ,6ɩSUxb)!9+ոsBM#Fzs`E\Qu/ `9B-tPޭ2Su.SۉEJyูź~yQʺZ1#46zJٙu}lous0 {m qY kŽVĘƘmgo dQX yh t`Լޭ` ` ڑ1`1f8M-#Yۛ-RCh tsЙ`]8vKoY*pnmc=%VeI&sϏbw .ϫ uhX)p" -W,Sįa2FmP`;b}‰+o3(ػ\rwUvM*)^lZz$ \ ƮFy2E=^<ܞ"0Ø\x]VG6RK{s\/npuiwb3e#]렞'[k=us(sTd ms;IT/y@[svTf ZpM^lQdE,蛆hFtAg12#+l ]`$z`$ݶk27D Q;ݳ[74C[Qcln#Fߘ/zC 32\e @9dRIb_8Lx[ 0 /$uƄ YXxh4cnm&17^4[BEOqx:%>" 8]ȱn,8”"sO*ݙbnk@KůE'ޛރw Zd<6 dv"r x7F dpÍ\.ʭXa(a6][;Qtl?v8 ×pn(9(Oݙo w>CdK3V.5zQlο1 !kno(_z..߷71*Ljpe3Kw #1v'sj֚J ޷tj`?$wo; /cfz>btfӁK^ţLw 뎿<0tegqa!hp7nuUdL Zun2ԁɝ K riԊCwcjF[Ŀ=FFZ׵qN׃>K8i1߶慈r g:R~\Hg3sD{qj@'hopkmkl\nit;fu?LNї$AnyݮpO .ͥQ݁ǻ7bĹyNXzoׂ`ؔ艕QiF;%n;]Q:!hcxcfq *xl@h:+!xZdRҚ ZVJTt<{V'/^,uy- A0tm֥$K'?r#_wT.epw 5׭-ć)b;UاsRㄗ[*H2˽tdv- Ă5&1P#kI ]Hn!M}=1aO).SDvHgpcY>qq>j-0{d#{CLÉ*Xݖ>m).Y:t{FaJ0B[O<>bQ"OsU| 03k`lrN~za5GzSBi'g;c7{x n V'L$2p]1|rtfˑ{PA|/n,V`kG ,MTTp2X[ܶ[ˤ [$!d-՚Y]7zko~ka~Y*)IӔf`BmuXN`qklQFх-2뎽)8R;.s"+h&ΰb\ŞG7w?msKPm_(\Wmյ`W.Oibē6ɺRه)9Y)t!̃|$:PkF^YNypV"[l49FC)Eڼ <^FCUֱ1320`:K%>9SciUz:Zwr?d^Lj3!iA9dxbúUQnNaS}= XD\^h+fTl/uoaa]%k$Xhè?SUK+JC:|1j''in9s"nCh!{u8R^r*cFнS2Y8nqfطu2z޿%kD z1|9FƥdҝV7]1V\fsfA׮V\xy?g뽣^<2^7ۮ@^0ggBxȗh_ߪuRE:)v진];~#痭>V(HP e-S}XM~V#W|9'ӄo7پߞOx: F8kZpJERd IDATʎ.`/'tKȾ:Hq# "F,؛ԣe 0tfvO)Pow:A8W,Θ) %.cseN>9=sҹZqe6] Vq7Hlh)~L8~~u,WstJF)v|fI .>/I,Vur~cA{;$0,S;TqP5fLj e4-vKؗ$wҩi^ZG@d(QD(8ݾw<Ďօ~VDŽkޭ?N`$a({CqCc^QsG+i~dyN8W7sbQ"lLސG&5n}8sS25kjݟfjdB_0:C `L^e[_-,k~8 `SN! D`oas`yB\O]G4X^rgyc43v8B EȘ2i@"[=وs-f苝Zقo[);u%өT<}wrtY ~,hzoc\캛et~4ո,DRW%YWj-;5!X5\ݖc@o@Fh;.]<2Tp+7G'xf+֑j0һH*ot}i\C[G4n,&twA)pXm#ơUQkC0bJV*^bU m*"1AabLg1gM$G$M0{U3M3;DTK=U c(f_ DUn**ܖr%Oaq/$)Ai!kc.yNǽw9y䔹o%^dNeB&F;9lC;~x:n2mx켻B.St8:4@뜽f_,r k^q3w >Yܝ䚥֬SSnaEPJ݋J:3 F+{xرuAuBGMڊ+tx; |Hϔ*GnJ9D9I䞰eUw,֪S lZlmD ؜)emrNL#Fe mJxʙ㎆9[PaR~+`uTw2} ; x/P̂ʹU{{ܔݰ|;m cLom?Ӗ  96hMY%tQzbx4JVl?JЯ9/̋L)açHk#@b?w\O Ac|ӚFN7j`&C)b&R5Ԕ9c[i"ot`oyq@B/F+XO|E7MWˈ\Re;'Kn~v{}}nIqInᅰu7)|L~fm7wi?0 q"B+vm#~ǽKv FD|`ptԥp M!aAt4TwY(J5ۣBCsdƒG0iJ_l#nHDεjIX޹Դ1 6gn)E¿no /sZ)ϗ Y ; AG|Rp]v9sQyH6=8Kx:E~kD^ӍLSlteׁ W{G;!**BQy*`=h׌10/yO^\cJGB[1nk@ܕUGJ N>2f={W/xD/xoR hfGۡS3Ejc:;P .{=~fFSnBmf5bY*0'}:%L?)"2ԢR54xa>L)c6d3X m؊*I9 +y@T[C;ʔ\XTnV;;GdqTyLvr B!EY';~N2ZVך!iwou+ǹpJk¿Ď^T^jyXƅ{Hb)[4` =4TGQݘ%H)-v|L>VJ|]~{(-SQ1`/\3;|=iJ T]lLdO Gbb9N2E\K70+O -,grjg7m($~[s? 5Ձ]hn>À>"H_W_ާ%:k6YlPKu1S[—%e->Zd?LF f `#ԅKw+K#晞6ٽ8yf%sΘ;%vD;|a8+v.=U̩]BC:ui7 ݥ~ά0˜$R`Mр{1-[ne5|Y;7M4Q`ẏU=m*`ctL5|YF2YGF cl=bZ.K[fC|.̇u=bۙqڈKB0GPrX8V3pǂdxV:E ui,iUêxqm)%>&ar;$Оߣ]usA gݧ!iMk1ԋ{^PQXg5Fw_O icM dc/I-_YeIXWVFe2T7LVN{ۘ<|c^A W=É'͗PA@Msyn= ;St"mろz xvmǷs·Df&Zaa.ȒGijcsЭ5Y;G8"{7f^K{)zr*ehpCupi_ `I&Ε'&~~'.P{=)l(n J)\) 8bz.z۽xV\ZՐٵN1m- l&zT"b *Vmn߉9S9 ig qĊ_v!Ƥ]fD;tZ w~Q1B$ ;e5uU:eKԐkk@cVJ"&^9lh>(N7"{eZCeX Oޏ;W`^sE3;|iXW΀7 R>BݪN9b79 ;~+R0İ9;Ԭh :OBDuvNF#7)݂Z;):Kxo䈔xSq6)|>6NZOKgx#~!dX=aS ̕<Ż}zǣ PgDҽyhvB0|dg49aG Jki_g|=gv04{Ⱥ!CN "i;5j|fvHLGbDFN+"eK"j4 ց^=^XETjkh3~Nn#X׳֎[i$fEi_S҈an&$͹Qr A#B&;8}oHH20X1| *pJInq8mVvI>WcJ f~ݿfm}DRi5:#ٰ7E(4V`]E9Ye@ګ5}zǽ2E\gܡ6p׼"6+; Kf\=|/1zvMʒGV;r2rtb_),N"gu*L!` {mw%7+hbeqS[~k^ :s㘂L2F7f:%Z _ryJD@b`&-h<7M %*D.O4 lit]v"uVB kd\B'nJU[}ĥU[d6'skY\H[9'xc[A1$]]i@LK;_azYr#|l yx}z0 4ԈOeƋ[C|̼=XW(_֎BKcDA- A672E*M?tss#9';(na E I6r3)Y:=J)Y5ݟCC-_r '%¡r ּؓQp![0"+}3nfO@x+Wq)5f|]lMՂMeN3Ȗ Tt}'"QihI_la) ˴p]F*q;pLv`ϴ>6b^y?ʱ((G+PO!DvLk-)̧hV]9EDnF88k{MNsQDVLJtz#w;Vt?vw4Ue`.5!r-eI>O6Twyh!aaldr JCΓFSTbn[A\Ǣ&Rz,,tD=**&G=JCֽX9 nNc:Y1Ρs4HPzp(1f-+c@՝RQPeHyУ `qۛJi^Ƴ?@qIN![騌 Q?/2y45w#êg:e':`5j}U[k)m1ѭmQ*;<*|{#VvC*ji+5듬B#[ř&7Ym3.Sd#m~d@5*׏9_ϖmsC+?w~pw9'ӈLOygu4x8}ߖ|b Q,Y}^%[r )N]Šb\U ӛX3r٢( 't3+V0j`~ kt5ExY ~t" Ue&X!/[˺< {])xXfbihjBbUe;X(w( d}H} l|hwDkhÉi#` }zc9x{egF)lߑ8D #3^%F ޙ+Y'=;ٸHEZGVuIJ=obzAcDȑ}y% ïkkx}X5%q߬{i> щWzڻluNkBn[12ESp;bv̤Sy9[SVALCm&Am5z,4Ptyz: j+ϵ6l͵BxɃMOĈƐqu$Ab@>JcP}F'tKed$Q38 JP|9;1)Ԏ-6gٽ,k@+ys>ܼ!D\O))u>Sz,NtE )1*/@qQ:x<swכ" ꇵS^bC tnZC+N3X5NJ1C1 ai( ъS|\s+3*+]d -e_ַgCH<=G3(kWpky".ؗ IDAT }ߚdPr{;3JRݜܹL'c mjiLSc"FGYM=ÉiInb myW "m1G콙PK ep< ԞyV7D8ܚ-Gt;yξpŜ"M{*j,BjޙZ)OժdsbJ by`܊r2[JKH] ^|YTpV*Ҝlq$f]"X4Jc杂36HSSz!^"}f]/!ċ"2Q7Tݵr#hyi-&bd}ݥS⶚cDV)J/1{B%'j !XPgz![MR8JZ#PvȲSg :l3u 0r '6VUw-GODߙz4J.tsC.Cq`cka46V?b^:)W|0:7m鲤_8WeQ< ; "KC_X{JVwuv>{QCDcnߑH ߣ`6,Sw,u|V[h]yp'Ӝ<+TK/䜨%Pw+ɆfGW;Mڕ1 VxR?l3#O]k0LOߞ&mٍ*ο 3u.j xǂc9L> xcM6 ufS%G̒4Pc j 5`37؊7.u]ᖒWxXvlƂWj WJ 5SAqGۜLkO(X@6b>e[e;aS t*G4 U<͉fތ%͎OE%jTDz\{шAjGgq4jz4wQ;7]Y nXӥ7KzHN2IӦrFM6ѓ!XʽN:lm ZU\Oqp\K÷?lӌHѸnXx%6N9bgX3-bJ@Kr8t(? _ω#0y֑T|ئ@o-`(ҬKr7y[[]IS4>e3D~}Jry0p߇NٿYk!5\y`.N yڛ*"yԐ}^eɦY$pkILmhF. m |";) {>Dq '>j"XF(j,~67T/FkqQ鐫}0DI_RE!.7^A WR$p?|G ^NP#WNEsCz%~kU*Fõ-9" l *lx;StڌS0axR 9gSJQWuo!ٽZٚ= =&nmĺwd#Gi8%Z]s("&\Obkfw k&xof ԝaQ]F^u+-x2] 1C5{O 1lbj[7zR;pJ((.t?W*P#ЛE!FwBו&5Lw1^eBVw.Py}^ڥkhA? ; @MF)gb! !-[dS@B7gNы,p4Zbuq9 ҂LA[iB;|/D2vz}<:'vM`{c`L+^c@+l񔞸Q\8*B?Dx@[A r0ƍc|XK 60fRȈdZ$nԔ.㹖)mmNHzW3 m*E jAe VQ=oCR1Dv|ӾA{ԃjF qQt2N2Z'v*C3uWbLye:i`!3@ )?q^UI\ 8`+;c{:|⪑.ǫ#tXX:`S7v΁FvA[eNߛX[c̒ƙcW3j"0 yT`d IirȢaݺtU /,R;;1U]SK:3+RR:[÷K¶7$FmaĽ5?זuۛ| B+tz+^H=,R:eu5r5mĊ:vyJ;o<ܱtw7Gv{K+ǘOO3tjfO iYn9u|`%Q$)\$qi4~c {X-6 FHI XS iXL$n;eB."p4,#uH)#* s/Dad?jGlH8dhxQ1IڦT)::9 *u ?&^ HsNȊލ$vtz ]i`>X\=_;tw0jy21 ڎ#f3]>Vq#ǞL*v̡ L(FFHU;Pݏڵ/|9=G^LOPjƀ>9Es.zbT6QgV;%m9Yx߫ ·.pcuk!n:abq#GAƀp;pv_ ռaqD)Fc q0k鈖)Z]絃ᆭg9%wsDJV6ʃc:8%;l4_8v.{Nl1fQpǿR2Omܤ,-EȧS34p&>u޳k}ы8ln3#6Ji"c]qc9uX@sƜC27dYXEZ&-f{}^lvZQ6-5L@\8qL֙[aO] 9訴ї)㔣Yq.)՜Fܷ9E|d[lj r]\6<"^L69Ax{4v;$]jMJp1H \Ā${﵎ "Ffrmp9) HUA^kهKw8Bmh=` i1G;$paq/S"P.>DidlC}+Fkndm}22Ō3X=.SVwOG%Ƣ)vܫ/~سa$Η|-wh,}o]y2N :fNl;)Iex9AuN[Gŏbe'v̡hyeFȯdmHV0<*ħ/k)O8ҽZXE{3PNM;\XSay)YS!-?nNJZy22 Ͳd#<i MQey:90l̑:vjE-;PѻaK{EmsC%9wQAR>bBob5Qe'q`ƅG1xL)ܷb]L F6]NP2ur{}/uv<[ f3,6v݄Ol{kH-Kօ0UƁ9)<#QzG'HKTval> 6hօs=ҍ+JGo &~P0>lbx\ʬ}҈RISbD>Tx|ѵsTw|~Y(B6ň~9d+6;׆44>tg)onfj,?C)%x-tk!Nc,_4(LSFjC96i!߾L||Y2Dܷ#rCU]*~럷_&L bz#3L3ѝ.鰚;OG59A-L9-Oͽ[)ڸ1ԁ;fWJ H(RA3NS_ex[RL=cZ9ԒXQ?0Pz6*'j0jMǑ:@K (dnA1-5Նcݭ9ex^ &A5U =D`8 M #m_IB(NF+FXF@m&?8("Li7%  ': v/V$U]R+HPkdjs +Fb]k3iN&G(ϖϽ=|=);dJc194 5Bgzjo=L{RFƊf]"̘qѬ }tDLv5#H+r*R .=ߙ;L?d# *sW-2r=NkQo !SwH:'HJfgy Nܰ_넭tnL:%<^Py}^m ڐl`R;sh%-ʠ 6f6^@50&Hx; C*yW!C [f$^ۖ]#Q =Ud#"Kj(jm /܈a._)=nmsOOD 6{|ϜbV$ni,O(d+ C)|?g<{Wq:lJ7%o "w1gߔA{Q*uĹh~>%Jj%Dy,-: m+XKuy1Q1sNX!/nQ=M5SP;r [4xiNEwELpQK)1{E+ɮ6[h^kܬuq 0erL9.8XΣ4$Z [vk{uct;* M1@gṳϮse@(&Q a1G?v\˼dVjL`'r=GQ_'Ru^"JzƛbnLq.wp+#6#Y<2[D6g IDATá>rL[C[ށMR9bug6>vQZ7 `Z;Τkݫ\p3Ro[C@fF9YG-"[i(¡h;mےq4_';d$#/)YmDvuĨ1UptM*ZVjXbns KU'CsFRd3Oٺ;43mΜl7qFAtM롳'N7Gn[i[)S۵W<NFt<ϧKsL>n8"n38;{]KC娨cz4@ xpqSoTQBD ܩEZkL[m-t*uY7\?u 'DIa/k*U[u#,$ĉF5JND ĕ6Hō*ʾ3ֽ1xG s`Q8 c[VFxcfa Lh1:%޻>1Qw:*5>(sTdkl,}zsev,ޡRѷ#&ݨҏED!ȱ$G&<%qD)оk 4!Eրi176ZG?&a ui/s! = tLfت:ap>jaIJfcc{S1>GV:i4-6 eo\SDeZt/Dst%|^ׯti4Sj_&\K64B!6Ky CL6:L%..8NTf>s0Ğrށ6g^&A'0;`O+uxqLi#BF]b~X|HU1lVZ30(0jQ|хj9 ?W#b=VX̧DwF+D&z U0J ο=G0ܯQ/1 /䁙  "%##CѐR@Z*GcQ\9Ȟ _ykW2)'#MYLm,;ʕM楄9b#_:):AL;Hu0ͼ*5cH5R ^Ur:BOremoN 5@_6D:a@w#ⶩԋ_Z%3=$=w$0<^v)H=o4ALR^K=J5ȉ)AguۛwǯŦ4'K$, eh;xJQ]WsF%y%̂L# Dmo ?C߯ȝ IV9 PJi~KeWND[msp:a)K1\"~Td4A N)q؝D-z;0m>o [6~96 \o$@aE[ŗsvٶ7ۨ]1L3/+kE&~>Mx%V 1Je!&םifݜ'j(vq^#w70N+R4PZ|&"/q%}Se%^ TWONݵ"~8[N-T jc@}2kw7: a_s ])QV@nM[Eքv^G ( aDpq >l<,X9N4YpS@ۘSTwh,gbqTh gT4Flr#ѠÌuC#g 9>ިz@ $`3 3Ud~ A H(q&smӄ'"G>2'Nϲu;?5Y jdJN:T|q~n~/oF) p߇ ŧ5/=aLV{1o(VOdjK3x4ʔF㶛"pFbD>e;Ny x j瓹դYa'gJ gq( s6WXvԪF%6( !RnuN,J{sBKe@TlS콣QcG{x^28N;G,X[m,m̄0\Qc`uN9xS9OivNuimahEO؀mxXfkAmq#yypcë˄vyqeJaS?g2h}t85^HNJ& idD#[YC7v'jֽai_N}씂Ɖ:ϱܑVx *N# ug!0v ԛK^utݺ*R܍${"86x]Kcp6ؔ3ܴ+T\r\ Ц%[ѡ?1jx9S DdV mpeo& ?Nmonߛ=18S'Ȩ٦G-:V~ , IL) kw|mݝeLci;bnD6zj#&"LǠ"#85_k "p)7ΆX (N,luŨe6~)9eu@}-E rf:Gw"dPoݛ#]WWY\\()c#=eJץݿAnQNyPpcS86Z[ “I`dڴ;Ff5 xY4{m.zת+$DMoi$7unB[BSjR\?HH91 *{=Mqi) ϟU2 kg% ,GDBL$xgtJrx#|;gZsk6exAuipa4m{x^iI8 &r{( JEۗsw~.=l{C'ۼrP7s+T t Z:Dfot]ڬ^u&'n-[5M"Z+6KenN)cm1$<':1|[n! _ւ瓹**?B S16:f:kɁxbflYtڒAP9^8o-Й"ܥqSM$5n#fܐ `wSAL'߉`߳p!m#NƩv;8:S4K=ΉQ;Ӱ {M|/.l$8Gѱi#iԘ=yS P'>7,hqd%.9{}QC/L Pvl^Zq=E3tT{@'7vt|fb(v>7]ectĔ&S5}> ^8)CSJu84B5|0R,&(%|du+~gc 3!ANRQX&z4H9Yߏ/0Iy5<:ZBLſ`emׇoS"VH`o;2x}k,tvVP%Gsf#gل~}twֽ|6GJ?ˑۺw&cŐrh1Ħ2mZvBuF>dBl VnN Th>xPq( sx:%c4Ԯ ҤI;!"^"Z2[mw_m1-Dg`aC.̟\K`)M͠owo)݊}'Br%g+UQqm&ޫua1Irq4&l6T!r!^a(uI!xyV9Q6?o;f+WTD)7vneh%mdYg`+Ҡ]ns_2{|?~[usCmhUhKP(ت&$MV92@\c^BrV VUrreI!{{Twvhs- x?w뗻f*o )6{J8ڸNNdѫ8xfxUToNJ$hRC}x*^e2V,F]Q$k~9%uG%GM \Է9]lS]= T(Q΃U y6=Hh~=%u":NjE޼[;f'PycHߴľ_}?g^P=E'RԽ1 277:\b":!8TWudb+"޷ׇii^]nȑRmKyMt|3Uo{sVo3 )_{߮e2b=s}FT<7 -D>B4pdi F>c'F#;ꘉ,YnUDٸ:,YZ%2)0GbOƟ{"㞚#I judn?v jms9 M c@~Nx>ERu1>um()PkZFMn#SuH/X#eH?q\uS~4]_u5=gQ|66AηQٽ3k-)۱ 2o݆kR$,jo׃5uxgf&H)bR1ָaՇFH5h#d/.ǀ. >tWuK$_ +fW?Y^kfFev`g^!CU],#RCQ1XfYyR,d*hAX8G+5U@>Xn—Mv(DQnDw/\ϰIxnv;Z#+N eLĺ셈ߍ#ٸͤ\v)x8?wc1fwD(ޖ̑ӚkzY gM0Je8恣Y(qs ~6Edt}lk ᏪU EgI"7 k'vWt\:p?fɦ4֍}RA0ňx/G<mN/|Z;Hǒ7=ZeS<¾;8uB,^{aw?^v$IQLa1UFM X'gpaY{o. kA7a%qdIi;E w98+]H%)cWUI@ ,jGxiL\ÛHF)oK|lLmI;9T1΅nFSEui %a6K1³ s+DDS@cyHb Ux$xw gxDǴwj4@LvGsz 3?ﮀgtljVYp3nǵ?n 8I2Uiمm:bǏ{Q8R xic7[.‡6ށt{atWܖ t:k5o  nLFPl9^Fニu )RSshʢB~ pսxRF:?PaϷW|S1Gԕ򠳕1N-OnX #2Gd:(b& %7Y Q.)^3 ӐxkTkusL|; 8ƯQcqs~/ITq = OWL2zBR EvkEcw:Yx-~S̉UܨQ+="yU'˪TuLc+ w[y egӐ8bi I'o;|\ׯ!+yoE(h5AF6@.M` -zE Q.3wX+ MJ;pSZ (@PM\=6}0ngi IDATj~ ,jZ{L>2K7.YsԱˆ@@g Sh3Ef ;'vGw*$LY]O[wxolG˃]'iR1g@Y%Va o4'|$@9CE'|>Eo{X CE˞}M)q{k^#g(@TE3N(=S/1 %]heZRu>-IFv3Α&P~u0ٺѐzuX}l:f6Q|Ģ+y&|CYZS'TL)g"4>zOFշd Tʌߏ%_^: VhS YCҸwp89AKg\ShKsfiiR.wɩ4`M<6[x2/QD|:&;VԬh'[|ڄ?o _/^4O@sˣԎzut'̊T39-Q;mGuӜ<`uK_uDJMDYdH.>_ g=DإNԝ+u=F%GTc65˩Ka0:1@E6Ū!nowpS%7)rԷk_6'VwbW|4aM}ʅ,ULCӦYk [*Pdw](kL 53VD^bMOd}^tҌfnY,2y^: R>n$Y]:&v-BX+V 0#O:4۩br(rN-qUJB{GB. wݝʷ]*#u tg֎{]k$mT:T<5_c`S먩x+4a hw/^H2`~}6>-kTVQzϻu0Z)bsS?аB:2jY(G.Hg5ؼa[wt0jf%'܎AO! Xsξ"i{*nr4[xp+ԣYe f M#o!:t~:|rLNQ;AcC`N ) ض{@&mf2pCѤ`]GfI:xZ"69?_/ yFZ'<&ļ> ֜t,S~˒;L}qrfN@;$ʈL~P#b͙%vPz4#@y8#*f)dg],{߶2֟p S YQ}޺ M$U65qz2MhI1E?EͲ~OqdZ.cW?S!h˕'һuPho)IDP޹ 5B#距ba񌏑ں`DdűҰ+RA"_GOТOb}+Sb)${!CsRȱ7Iwb< }Љ7M7v; d~1w`b+jmue 5Ģ"Ca5Fk4>\!|9mWǮikmh QwUl|687=bIH9ܗ:dȀр ܩցYUw}^{]Lwjt8FJl^f1r2/V2x? qE|Xt4X3G}i; ">Gn{2F`ήO~+@JL]׽b_VʙjeG@%d|QXj]'[= uOqlz2}~Ӕ:'r+ڰN};`@^r<Äz6ɵ4Mc4qP!%*_P>1r |#by\yMI ˣ%INE`sgZ 17Ox0A`mij h;َA 7\((l6ymil]3촚cR;u ֑iN .wNebpj/ILG?Y2E):1a`GlC5jl>?/t[%fwHDg71#cd:9zVE+rOl)R'Zwep\Үyf0i0T CÙLOV(IS1 00qB`Ji򃸘:^60:PгJ+CݍuV0G-w3;|ڜ9 |5%)W3 fWwύkТg+hjj7<w- a%rר#2HT4Ő8Bn|Z<0uax`Xs Gg۝Qd֩DKaoLSqge4g]N:6i6LOsQyPj¦,G ?|;Z!~:E?'ٴn!a54tuڭܞYޛG"C_p06~ah+M'@7Ks+ Նf%u\bx^$yZI u(>ڪ93:s;x{4  Q *n;"bR̐v""!1ch&DO' sp4y3bqd Crfcm(*ɼc7U.2@?(6+0B aZ?ڦc" vޚGoOKB8e.Bf`6PP\É[?S'VY*5hxH cEyoGs#Geܑ;ns:ōuah,qcz|0h1 ;:E.^0S^|95oxsT7MԞknxCZ] 3ՠfz5KV5C]c"?_*=vMQTvn9g?- 3V,:̕F@"yы=4i܊cYc\3Y fqX]T![i[΢CQ}\޵PveJOKƔx{\^:uT0`kEuqв1Bq?ex1<6:w{s˚1Gu )tur3 iT*}H֩DU1 ڱkDk:}݊P! ~N.e1\{SHowv7oGs|Hm,iv.^$a@vƳ7::=kv+@M]fֆ0l9#=jǣ5+ Z:H1b ]yvZNp#gcqeEB@ZwvG4vEq(ͣs [&V|Ӟl/[5|A1gG w39hi>/ Ff1mC)7SUģ6<w#4T{[lߦ@{>e@> ǯYe:ۉn>"u nJ}ߋ{Cߔ*3&l,n LyqɨmHDN1;oTc믍2[m7=p&ڰ/ xRr(qGd,d,JXC%'Oc5OK§L? ; ljq}\(R_Q߷^C3}8PTN(B-?Ҡt>"S4?kF֏*':uo'U댋6%/a^+lzor;tdN=Yq1{)" ae*.49!):eGOҍŸbˣ :biv'lLJ(U+z+qi;d#i,~e Q.RYݐ`'uU\fĶ"(> l wvO|;ӄO+Eo;OwBF #^s{i'Od@ѣ1;jhsZqCFQ~`衮əϵ,d9V;*L4G?kfe;nK+z[Dԇ #ԑZZ5ŚT|Ugo )QAÊ따<< 3"1 74`3.|D cAB+ bNƚek֚0)ắ?߮?\4(Ċ״4T`/eβ2@׉Ž6wy+XM"" L)t r7՝0{kؗIk9S$`W ZTO]IF6w(236IȞz"r1,X2twAmr{,րo쥻&.5o r@la䜮t6uقv^^G VbwJjESiXL(Ekbuhf)9E~y_ QOώĨ-xaA4XvCX[NzvnȔF׬psޟ!R2:jwW ~=3^0lp%^X\XG''6S{jL,=uRSw~PƇD<>`I4D]Kfcg. 1$jFs)O >W V1&-fk(W:~<{l649сQV?n;?_] Ē\4δgHJq@y ("0/rX @(ֽq}\ҵ dB. ѱS6o[(\zjH`m7e;ب,քڒK8lE<7kJiݥHDsV>"b`(قvۻƀSkŻtf mMMV=`> 'zt_?&KaY/lD7% 1ۼإ~9-U/?-Xp?잳5i$w?;LPvt?8qxpZsӉ1-ϭRϼ9`y[0^ c<{QqsPUI$'kF E#∰Y6Ǵ)D!ob j]/ 1 xۧYߏ`nXŋ/ ɲq}\ei[x9rɘS{Q' RhzG(m-h.YvS$P|G6&Z2Ķk#ȼ=vf^>Lwdj2A6P휃U`9$64u]vxD"iS:'uԾڱ:4bf:4,2޴*Y~ @wM0΀/I5GE' (za;RymcKu%E<ܗPC@xO)b+/o<4v֝a@L Л= !t/G䀧#]Rk9YUMT/svRa sj'%:ɿwބ0r^%:"Ƨ֩>5lcm0O"vR/;ɉGQt훵{R*<6tG@Fp'$51zB8fJ9!cl`l0ÛtuP#ol° k&9S!SOɇρu En)"(úCB%1p<}yT|ۈD9XҘ*E0HKvI͡]HJs]srbc IDATAkٜϢbJI%RxVMK#K,bO{q*WP:uى'XG'GWGW6\w[ixN 5 Y5ϒ<2a$L=:^&I?bQ5QQKevVTmGiJ*2jWp .<)oiUgg[idGg2<)ٹ#pR׹)Ո%%㱛B4eȐ>XxGѻ0GC]1bM*.C]EsHQ$J#(6~x̕d_xWlw/-?ƀ{A.6=-zƣۭP7#X ID5-yQZ;4fcuUja&+z ;,ccҰV3 Y3[ĸv\ aA׆6JlI%e[U@S̲$AeƈB0ǀԃauBV<x16 LLV@H5i,,x= 1F*F6csXAD ߷zr2eNn" N^i{˖pVeapbߞ&s&3W;iD^[?^w:6hc\],Wmdt_1V۬@b<l0\xO#1g Bc6Tjeia?OKGFg>w9$1łmChViw`yNCF|EZ5'1 &-C q;萵G^qVAc55Q[EbWx?b{ pY] xw1H繿8'>}\e-I,L=$]٠a d߿k͌ k' ao{wNWQ_. r:Hwb1n{Eiиӊٿ[\il^ZGg6g51+9E@s7R:0'3b:/c8ֵuJ~ZEkVpk2 f;#e8ŨHnʫ\[)WvP+(f$9%YuQHs.U/Y#uU {"~nL?ayANǧ%2İfv<3c􍜅LCi;?U^*"ϰ">ߧ9=4Gu̓M ڈO{vб$1XliQͺ_K4GuxqIQ=ўݽS0|hm sGwIQ1`݁Tl2q Y^ìB:A$cv7G1!q!8v;F6ʊowehp{ZGMXun?ٵi|VN_'BFACWdz#mh8~;8αuļXd9װ l~\d1CӜ+|Zӻ+kGutg 7&oF(,9u M*bLhnREh1E2cFK̺k%{^H1/4 $DLtn. 6[sׄo:E|T|dtqўgKyU'nLxsQ: ?5`Lԫ͹)|Jܰ;IhD/Qލ̭0`p(e&hTp(SEN-FazENP)*t9!VT>GWs&/0hKdjca`>Jbc'q  #bN<oz `ɳZai ;3#ci]T5h!Qj* ֜qҼΐ5cEzϿ3KBޛ"5&^i@ﰪЬ4h:#x[~ 5ˠ}1;ǫ[t?4hNTYW! !Y 9(ukh.7,'{c:%>,4}kKu۔ofVhN~2٨qrbkjeVbՔJ >2CJd+=q@C c'V`M$~**Ԗ<`鸀wwcHӜpjec{J{wM?r[U<7Mj Y̓OM֪-^nQ뵇;{5`N;RsZb;:!>j*9#~ qEx*5[K~y4_'^9CtQ)׌uJ2:!UKdSO@o߶QK^/n_3n&Yh5qpJk(?jps qȇ;m^]%sK^]b}h b'.k'gE͊1O ?7kRd6WZ},k/h'Rh\vx#+;9p냋XyIx݊ >=*t[ɸ5% vĕB@viҾkw2{W}k5kLSd8A rHR}UѰ$#'Ii B'dNyUg Sv=/oI go>Ed9u-34#%*0PsNۺ0? hʑ.0 R?NLYHT -4,d R]եR]fКnƅ) 5jު˴;ofw?=ɰA,E-W׭xр/p3u(`M&ϰCy Q!7uB wu_T\Dq}\u9׷ؑI Y,` dhݭ@Dh"âJcm#<(+کUqkMP[t~ Q(ڇƃ}ǛD'հs\yJ]"K(=ۭrZvzdo׉a05G|^9'|ݫܚݨTmcPzb@D9[1\:W淦Vd S8^|D'}\ޕ#`@܎\j+xԎŋ.1̍fz.|2}7X$[{7<oGk\dIAi3F{ircr9g2c7*2N};~n_. q[s͓ڷȃ׭`bhIXCJ;>4}3T=Ie0sLnU[Wi3s\${CYHsP1c2;tW WqAZGҘts{x'B x^hsӒnG{a{NgDuUvЬkE(׿Cn4 'nO35v^J:j{'`[Ĉk$6vC2 PKrr?.U]d-W5Z壖jk88-g֖Qs5uN~5*7:E~TReBߏ%sC+|!&Ӛ"Jg-埏ϠŎy@rCIX~nz _,E3ẓ욢d{e9lrvnE:-dsQi&-Pn>OqqJi>V=8g'uoѕSpoLjY4^2 %-!|N[15tjFx;[kkrDGz@U]JF52 2Evo2Ӝ%m'Q6 jc@՝|XU@ҬnuL?i{aN.sr瘽~566G*ˣ1\,x1]S,VME*3US''A\%[wUu HƦ$(#j4 W6xJi£t|6y qJ,vqA>u|0œ#S Ǹ=ziR[4kO=*-ÔXw{`m#4M bkd sx+Ջ#voIp4ͪ9+cJ ^-l4Y4)"jI1si(s?^4G3hx {{H]JYMR7b\E|8oK- >IO5jcҎo_.;dXr]o!zӘ;+Q~b Q9.@ ;!4Lee |]@eH 7udnyُuYc|k_Z@\Fr ڹlYs}#NtEoORr>4^j>)tCuDXhqMܱ:~ ~+ݨBĆFVдh"5dݪ>79:k"6l͵1)@\jc0ZA3O-ҹB!Cr`+F1&)YSA@ޮ93\yDLsE- >0ƙ1Zg|fNřFz˽2q;;/)b/6- BiP(z1H^m옖Vld8vNV*!Nf"p;vo;Q&IFށZC(irnT(,a{Zq4@ۣ_'_oW1v|w|\Y;~)]Nq(e;Gup=֎hv%1ÜC_zt__0jVLX;}s锸9ڂvv[z;'<ž62kuE@hns@C(P7RwSbCILcD'G:@JW*<[MR׹lUPFhip>dׯ!3AhMtɲIdFtsc'DO gZ;gXܚN1 C:GluN:ߧFn3ޚNvvKcjN<fh;x3cFnŶة3d!ƒ 5ML4mp܈EsYaUFB _0u"Tԧ]4A`1L3^L& QOKUg9:tCz| AY3z:.Ӝx4E98'щ7;瀷!uWّ@c9JpO;6|MAdsͷ[GqǦ{ayYϦBDv0猭Tv\n\=r40cN+%,`&ޏ.mnV;/Ҥg*/Y<fYС6f[%tus!e וiK3Iݔ6ahv/ȓfwW|Ygve \5`(S,^n40 2&1򔂳Y V 7^SbaQ9H!OӀXV?Id]+A? Cc D|΃kFi S }ҧ%yT[##" kp??BdJ BsgRˊE)y?<%lD< P]6:,<:,/(msCEXkwdqV4$l;Si^>LksX`%e5'+\yJԅgy ہٴ\~|T q7=M{Ôifj㺬=~._3 8:n &uI#%\( &USB4W|$?A"2`ReAĈ"G݁95l "Fn=K+l;;SsGVd8xCƜ9ZF*1PIQG}\{/|ܷrچ˜ROw㤿Fo-fu ;I ǡ)t$Ϊ[r׽`+,uJ/UZGB!2ewZSi }@o ..$vG%UbӒ(2G:n8vED8YrMCf[sYs @$ˀc6D>_2gcxxƃqTzJ?vTdžXydo8kIcR nV4 G/vps* ERۻ#`/*PIζ0)EϭH8mLl(OK׌x=*{cXM8k} sS1jMnת%G1X8o;7Y\7H%ʵxUuRs&HP Bcn{ul`בTR@ GǶwLsC*B./c%&=Cz.2%<`rV"fp6{%ǑdIS HFDTsfvdgenef/VfgNwWUfF 3j<)i`T?U7WT!EYpGDvSp"gb>t٩k5 jL<"Q=^bn(H17sߣ6sn,O,/Jx`i6c\GVb[ 稵GB4;Xbj?.toZA.mOYB5|sZ{,zY2Q~3 ˒qw4R|El)zj֑9[vV (0,၄(тOY9I (AgkѣyGmBg2!3G:6~}wBqJu«u Q;QMnhk d}n"iWlۨv.MB8Dp#Nۻ,p- lU πt~AS=)&Iy:%\#v-Q:S)ryܵa SӹVtYuVjmX>U?kHE#7=0w,10aDh>n2%WT) .3vJIPx-R3͍Fs C!znEZ #\F|2ـ\@ΰ0a>+s@xu' sN ?ѨmUfׁc)ZINH)f "h:S_66~dL4f4n`D##X}WnS ~B!B cAL7c)v5P߶X9T brϺی's0]pEAI}u7g=s|Ҍu%mnlH13NYslt2v)yJõK9Z0ǀe2w`vc]g XE_F\_9ϛV`/ss5uo )Gҗ8V KvXq [<[r0.*p^r}GJnQގֵFޫbnR%e~8(uDׯwq (鑮9C`9Q6#Na 8E e-F}`yszIө{7r:Q[g "qx;KmN`w:ޙi]BºWjzKp<l﫵1< F.!hoc^;eiX6az[-6C֑:/'ǼvAAt~Bw4bxbRau` C3r%e޴Wq:e%MXZaf{CF`3UO pG[or^1 v4ڊatnq##;zXAw"QPas6m󃎸d@#-uuo4 UTI]l1N:SlZ?T,/l9?{VlºZ&Ҭ6O"baFзd:+BeAQKd%;3V|oapmdjXsXb/ș:'ęɌ%ScJ︰4Ci&?- ϧ솈ׇAlZ >-fDݶ칛cJtKN>7ERw-ҠdrDiQ~Km0;хS^=q~z/XC)t|NOu#AH[ ##i [S=[Nw0bXb) ==+O)|Zj.Sr碲k7{JtnAw,Sz6؊_; iB kEșnO)t :AxCn]Ơp<{w{.33Hx}T\32bлĎ9Ԁ;ϻ^mO0x] VpbplZ'i9g\ׯtY=cSZ-X6q1 [h~oxU(:gȽ(@dĺo9:^d=(*$Fh>hJuFՈNk0(OVQM(9N_ 1(9`5sD7F6NL)k<΍ЭgZE _.٢qY]x—&p?pK#@Of:02i(6cUQP'IcɅ9z&&IjJxm(~R$4Ua;zJ7xraC]!5T1trŜ lr!݊eur.wI~ъ$rm(}v֒M')> 4๥U<|J=sFؼ;RzDKf ԗj{$5=+i͂45-a6cik$ۺQ1x>Q(>"_}~H teeBR{2f0S܈K鐛.b8ЫmmwC*)EiwC+3<)uk?w:y2--aL)6rBYpQ& llaY2[zf3A~;~<0+ʔG)6EQq?&|< -Y^s2΂xR0İXApR1(.+ <8䙏56ϸ,)u`/FmGok(yO뎴iG[XO7] B2zNѻC*6T^RtVmko!hWQdg뀽&f%&rQ}DNα@je(ՙQ^LW2Oltq)O+FfgM!y:ۄh1V 1V*K)F4 ށH^ (mR*uhBx1nN_.'e#D"f+Wf0w?:\psahFΟu%iG$!"=E q#F(ppY!ZpX 54Ep1lK 9xTƵHg BChڂKn EtPs? خDIH"0Q+Jv#J0\C2C2.rxE8y?qaE4Z1Hc&ǀNǙph$+vr-cB\d)댞K|EԆiN*:*v/y8LMĜOv"Gr)ш@F<`xmJ5tt| r0XvO f MQwuVUg m+:ؕa Gt}:'ʺ> ߈ds=8 zu[.s:b>ֵQm]}0>Y|:Pu9>eVoh]"a,G(9kKZ^2%+n[B\''C&b[,Le:_/S1V:^K9GTܶS/],ڹ ܤ1|pYS *wTdw&}kkl'āQYIX+,D۔6N1m}.\'{V` ?姳)~p]eB)a"%Ht4M)pOݳLa.pskd21AnUVB0EoAFM.%ٌӜp"şGϭkxZOU6ŏi!r# vJj9 - p:_PTYXcޱ+,rN 5vO)nPmRhzaܔm-8{Vjn曱h=O"Vz%{G#ol\>|{47sX4,1`I8LL"hf;WbŽgwB Qfp̺ '^jxuz37E7Zycl'Zo$Ck-'zU̙\Rr>bۂ8wSohm7ݣ6,e2^J;t6m 2[- 0KU_ǴVr4 !zP&zi !&>_9a쏷>%4 O01Sn'нˡnhVt] Sn)P͉t 6R1MpAM[(pN\K R7A=2:(Z =9NDF.q\ykNr"0eVH>tǛj ب`2Hja҂kr4 p ]*=Z}Y> aQxJ1)Nj~3g_u(Bqͥp# ~NWvTĕ먼vuд9[a ntُ4dP#qʢyǖfG?;5[km;K R}?so3 AUw1*jL) aVŞS8b/yS )估sxj^OɋֺL4B)OjVH qiV{dux>6O3~mE~N~0uL0u;h}Y2x/x(ӊuP}~ `ֆ#S.*VH&8.N*7< t`S@7Q i``TTF$ 7<Ӭ *$'2Y<ՃZ .Ҭ˜yMC@2.urS\mNb(!~޶bfg "6TxIf48+uj qC9*=Z tSJ}֏zVNSr#G  >q97nwa.juv)`'ôaറz4E*#/㚢Ϣ!;<Ն\aOV;^ˎu< GWUUjjn 4gxz1)GlLQ6~/#|x)J#Gi^qmPN]/(|V@5}.N Վj؎u R+mHZ_Ӆm/ܻFƠ\V7eT*ىslA52dׯym,P*Of>B}YE4 cctbbFly*붨O h>nV1nւ'9C @w}_'2 }2"\&ʹs\ 767~s\tR:~H'`+ӑq",lX9mnxM\|>~J#6,m;"V ݍr/v~{!G8 X⽚Fjk ;)9F"}ϧh')bJ<?>"@G[KO0EK_7b v|00u-mbE46*m,)}dF7YV!SM$KYD{{23#eCU,s:l>o!vE[Bd ڈh%i19ď{1;C+Qc{kXzfˊf0a>N= { >{BkƖko?a72.sr.m ˜LrݬtNqЁ]htlstΏutB0FRB} 5ݷz(LK4)j4ܰ+"Oo[;rMr\dj7٩UL2{}V+w*"OtA ̷>D*[Ҭh )alNC8mܛ*,˴PdRGjf"TQ;KG01n)Y*}8v5~#>-;C5柍缽?nmJ~P?n9P5mǿ=Zbҩ$!V;qTÜa"q`2PsXiHzbȔم2p|aMZgѠQV/FDR?vc}?_ͺdĜbfmsF]QH˒5P4jV 867(Cuad) s[N.ډ1x'G@N1r "@o*n[%k% :s6^fpwi'"~E#}}8DX \uiFNwZNy{T) j5=CA|qG4 5{92h"dc@#O[u6:6O݄΄bt S5gId)x+ҽ3Cbkm pۚkB?Oɻh[|Ix{T"^*+N"$߱_ |*&%Peɞ2p"u%w^0";P"@1l,F[O!G@mGix{%EH J}6ҷ#jBCkӍӓkH[$3sխ:}Q9DtM&]^#Y hl*$I5 uϬ# 2vu`( !fr%i\ R;+\ tתRch8aQ-G^*1H\{#uo(0ˉQJqv/kr{e&%'734)3GF)khL;I[[NhWvYI0נpZ0W?E19~duZ-=k%VlϸUΕZODzӄyYUUkǾtuBDGʶhl8^i=vrC̾XvDb0L ѣbb[5;:υҒ,SbO줨fGt{x2(9`2xHacuٖ} Yg⾫/\\Q-^VbWRxԙMيGeJHixkŧ%2%\&;ٞsb;!t7#Dn.Hzbl5j p36꟭GLM\&Bkl b&][&T`ghrIj`bhtV1{.Ҿ[쌉!fVs[; xN 9V2Z>1m/f<T=jCGdWp-rbkTv&cuT|(|T+.SӶ telƶ4O>1Z7؁{߭dȼtguM6L b> 'E*׽b 19&t?HQj;ͣ Ku+oe-'"Z*N߷ޱnHu_^mGb)>$xddvsJ)mKQC8.{)8(R o?c1[m΁ں?O)ѵ*܄Wyz8)&19׈@5ӌ>|jպ[^>MA}9%UZ[^c:'|6gj$kE1:iPZ? _oulU#e:!y)x 'o֝l4Ψ$;<:M*И)\}~\/rl1FAS >]bňa$Zki x 5%kޱQpcTH3핅53I b;)؂HPHۣ.Љ!<2Gsis2IAv;19F&}nwJ;)aFtp VIGiƤ3)úu #Q.4Zc gҹkDE[le7g=8֪tZ>$x[u>/1vD 4948TىFuL|tu+~>Xd|n ~)3rD#!.StD#ҡ }V &)-e(_0ܢ!tFDh#窊 `<ues^f[%euf KGui?v1)huE IDATlh {"Q=-uNWQܑ־^hz`qd ۣ9R*NKhc%i{1^#SķusTj|#ϷsViN<4njU{@b hRNIg>j"4iax[OFNq'uǖFq:ŀ1%;q+x*~Lʈ˔/c .ukG ~kJVYn[#:pDѥw6./c9;Te'7;Y'g9 )bwjnE^#K1>]HiDH" bWӟI G9Nݫ޿cEtkivO]qT8B{]&)O0ΰn߀҈ ssF'`ރ_98Ÿkeis14f?ŰrtrOҰ Ri棢jSn3(ͩ9:}V 5Lnˊ|x0'*U2GU*ۣ 0A)QZG#K]^\FQp$wݫt-x$Q/ mLZܠsaGiV,am+c0lN N'vUn?Q+ݕZDH6QWqT;7C࿿ϟN~0^}o椖06S2Y|#pEd>:vfV4h5<.Z&Gm\fC:!~qm[eIčߋ;Ju^^ 6MUx}.qj֐]"@ۘ4z#EP` )Oi·[1 @s\Ukc0D"[16ąZ$Du}orɴw0N1wG!zbf^#ZsM6jHȕ=_RM av-66he7PTBr4nk5^`S9ϧ=G*3о@(9f fqg)dx/z.?q%{,Ne'e/|d xUhܬvx^ˆ:儜0fcoFWsk6g:Kc(=eRl4lpѽ6 :Z\ٹ,G{#ٽ2s?#ȗK;GTc ?!;#AWEUnm+XiKxT+Ui3a<36yGe_;,92u:RޭTD{11'vT:7 . j ٕ$ %I<®`dsr0^{m͓7zR47O[)2e>FQڷV6Q?wwW;KwfXZܴl? v6׻[uV)9,:RC)OC3gph.:RS9ŤDxm _牘 ncÔp,\䤕 ltwVi4H2Sff )|Ro+X{z[5yMLo+_=3oK٥ջ;3rko#AEQmfN6>МFΑ@3Rpސ9Oz(o(պ :գd"A3ƈVp4!Q"u]r~/z^# ׵ -v8ƙ2=*~foE=EhES2pVyMđJ=DEt=ځggF NHSl dh".ŽuϨ]bѻU*X2YMȜRdWD[xt$~`fN.% BchzV ӕ䍇F8Vɏ+0zr2nmz1B7^3>)nl.LhKˊ(4ǀ[$9ڳu#&x0J{/coO^׈ _{`MIA^7{P)bJ#c&lZt!-W׀D,ٲ+󌯗7qje6gQ-FRA9ye HLukdhcntB҉Que{SJ 1!ƀ+^g렭;Ú -xK1p|H X z `m"kg K, (['xZ]#qE4@r"9F*n'Y5F`1'kXO6{j7G@f QMyj`iZCupT"kWbwϓN&?\`wJ룀M0s"h>W7b9,9j6㐞a9 m+b S]yF>[rdQΛr X;0aO*&6v@A(z9ʌ\f:696)UE,54=[2\Oɓ8\rNaZgvnʌt$+[FX$O[m=yGNbQO'N9O3{7>_{۔^NB]_s iktlsĜb6&YeH='v'CTl++*wc>GpX~\5XI||Z[CCF{? hpDٺRwb91}a#*KM.DYbGnm39OC,=-fvBM8fO0Jhq6m,j:Iϱ!xր2bren 3A^w|w4jAVK-VGѭULdJ0Xx+J]ͥB>([q^׍wVL< ޶Wn*og}DHW T%dv:U6 $S){A WT}iFxHY@f,3#d܅; luJ̔Bݙ%Ch4jZח9m?G,P}4G ޭYʂ~Jݵ)|,=z%\ W˲OqKuS] |1uOz(]|( p\^ɊQ\LӒݵpT0D,uMڽn΅nӵ{<ށzo&yBtfnJ'0EܷJO 3)7ݹp:(A+ |p߭йδp=%oTP5l:nf^v (q}\nnrEo9rlþLZ(S'siJORZpG/-}LyˡغA,(pA~܋wh]a#"Kp QB<怉0Lε6ءx u’-#C4%*[-5011@1J?H0dp1 }od?m&2h N݋t@|)Bis@KBe5wuw@p4lnި){gKVV(r4|)ks׆0sJs%C$F |W9OL  `إ2P-{#mu'5A>қwn?_mh;ԝmj(*L||fE=_/LuP I4O: :p{Jh? c6liy^fTI$Mb`c:NigL?IS x{twGcwMRUZ(ǧo,%VNgLHpS[CJ9E\cPj⮠o RD-.7 sZ?:bQQ'i$z{T/Srwm,C5X1$YR_t#Z{p-ְ;`y N ;0#TAl;C?Ӱ@5j#SDse8j2~x6_5rU_c"mуu`Ma#ju;6|n>p"ubK|3`VaD^< gJb|:^@d9+aPlR>벥oޓ8o?ֆ?ƞ|Gh)6oy_Z̉# | qO>4dׯw}dq+x~a jm)=8S= rp k1_&XSXd>љug~nB('vʡޠx,oyɘ1kޭ;3u7Nm{߭iIQ:n{}-,!Xqica16j]DX2Lv3S 'ɟ+ NS}\X>)[$GcĨ|rŖmֻ/DrmQkHJq7<2>-#J ݔ[vANd&F_{Ł&n033k9gX3DUvv)A@* b9[uvƏa@P5I+ hqG2[ X&F ?܉16jf6t -Py?]&s^q:B,CSO56R&l?wɁ^.G%(F(i4!6k"a)pk]usdTmSjd]Ht ݂;6+c"ku<͑8*^A?0XTaD,ˤKcGܐ]f3$:֭yWd;|s2d^E!)F/{ca(=%/)iެ `PVPyV]ӘbdI xTw|Tq/cNHm+lb(ߺV{뵊:#G7h{"(ō慙Ўbb6X;(B"uϰ 5O2> ZZV:圐xN| Jg=UH؉($Hofw cM<'Ӥm8׵8-ͬ v@ɏ*γu])+U9@;R Z;SEit"\mܨ4n#`3BM>lTYL?͉1Q ~U6<5g.0mҶEcӠuȫwjT PiŢWޫ)F$+S_l9i6/>UPݟ;%#C\(ÓXqTdޫޥYL`#^F#}l&kة}۪ F0tu6Lө8SNs6*, IDATP-ìH ^L6({m-N9zhSTu`,vD q,y!N˜\t;%[T>cg6.8֐ZN|u1'{o+Ie]TCUٝ]Ul 5A]]>LUe9z?$HwTEE= }hj&nKNS3xTcQ:6k7 BeT7 .T9IAZSK^>[2=eO):8z TȇgP >!f- ƿ9^;WF#R}9XSʉPH st+oȜ8m>F2F$NAI48c+U0$.4ϵJ3Қr̨x)pDh=s:6Tki_Vͭ|Q'3k&U˂~IxJ޸yWt/֙fr^] # ~7y9Zy<ُn?\}7YfVCl!Όԏ!ID- ;ez$%<銿W#݇M }U*4v ^M dX/dwdܡ׷ׯ_1Icхm ŭ ,8nY$fvt +2-U@AvxrB'ݎ~^I/F^\~Ν)"u=jD"X @w3ZFQ9d3u2j:xI v[T^GCs@Ey!@b^ܤ3rd O1|c]Y 8l [ h0<ͼ8e< Lqt #tRm,ڀUl'D|&gH6)K)hyTC1RܪUi[i6HIz j\-ëiq#'D>HICfuo[B2^Y%5YBx{1RDޱ 4.<8_x\+9&0nF>!UpLJh׭F'^>ya!rMZ "j|+0eQto1NrewIvrqT~}()P:}xPGV_/[C8*_Tư"c/90D"D%BMLYrT]Nֻ%SCx)]|=%4 3i昘mbV {cS9?EgzaCt؋j_$VAhfK rSr Oݖ-ُۼnզǻ`{g<,yߝs'rDw7S}BZld O6qHt5)qhpN/9}&SBV(سHH]k~m/u?S ]s;;홯kJ>kH#I^^HH_Ly9}/|0nOPν.3 97$:a IQ8,b4nfwhf\U°M#_=!dÂ;K#&Z;x1Lح6Jor&ǢmJji.:X4 F_m(-[(+ 3TxnM*W.i v+ 4WPW #(92oJ@Ob0YnOR H"lA޶#c|ea9(l+9}TA;{Q;ǭ0<$i\SDkc8qSU6Ǔyx@Zqm͍v,놱ݭe lDLV,ȉay,ك_hh` 8w*0nզ,$Mn]MENiD|ail 9''eKVK; `<2V)gsNӜmM$֬`N }߶ag441LBO4Z.l gg*/lS bxHRjwھR8 hKf=8:7YC|)2 3H+̒jg|Ή#jF[%c-i/ѬL ]`+"ÒYUSyr nfsn[iv Z=]~٪#m'ܾZjN9x K>,w#Ǡ F#ȑ;%$j@Z#l_d{F9y#mM+;緂tt2%| o_K ɀV{!RI܈y|o)xlßRZCnY$J$G j6|t(YYy)!H9rUꉇ{p`|Tl`sn2zVniFPU' vd:ՏEU#%dCQz5 ^쨫V\Jڐ6^"11Xb6')Xz< /kڪ=~yoH[:OіlDa.o<(P.f"9n(QS*Db]!ʗ(Vp@Imf)J/QV{g_ru4nb4 g)kq}flLu> <Έ:L1ZM8.3*4f/S[p*Vzzw0[J3Q5F86"z*U\I| F%" (Jr*Dc.DR#%*:h_nh'WR$LI#&li#3vՓ;*ڏwGsv٫]Z{JHȶG,NԘ8b*2ioѦl/X/ߠ~0`K?G'0wsbPGp2-sHbɶu6smvIR@ף7㒙gNEo.Ic Cؚi3xeB1BmV9ӌMIdbOȍYrn֚,X [dD >جM.P?)N/  yDrTm)+x {p9hm["FS(A2Y F#EP0Up/zݪ C nJp@h0 G= )@o3:%2;JzK1`;DfQBL f7·Fu!T it<C vPDmQX!w})G;euOz[ ~+q_W;OHܻcimpxފm ^_&{^*?O΁L̒fei@V=LJl vRB@p_J wu["'S:)ӭ XMUlMiğlLEpk|&+e{qL)2Ƈm^$oA@.RʐT\ Z*Z3/UPeSe4&-4BW;"BbD$Rо[mm02:#gloC E ~T"b%KW\ +Qb'^ݝrVH,G ; 6qGKwB6_U$+: |0bCdHQ8ABe/]!ٵ4* {%?@|+ZWw^|{k߰?lNU&--f ;#e-q ht)ZU@d3Q9^e)&; }񩔟{mhYP:lgD%z~[(>bjs5ZVݟ7'3`yr28>^V/тZ-r+;Be[B6Brb3/5DND属$n*nq v|jC-JJR;Γ)DqG;ʅ[&H5j[i@<짗g^^FTs SVw3oV@$N͵5:6{rkX@d% wAʼ351JIy)ٸ|u$;Ouk{C.9jMpl;=nYbǪ[0ȏEHS3lT&^9303)MU5ac)C9z2PōS{ԳTGDw>unD`ݟM2ӫV!g|Bq]j9iAStЃK^K={]b4evX(kR&{e8DOeH>GFs.ϜFFJ'E. 2a1S̞bAɨVzggqyHh*R5|qͪ̀ %yRӠ-y_9{XfKSrxNysPYI~6d(8eVx1=,8T mn #ɽsl4KaFXbf#Z3kg-#eޝDcuX倂6?2D NrPo12)%{E8!@5o{ӻoo_kzۚM's7`Q@bufK"i*eqHH0%w1WRDo>@N4I 6k^UHj?]N컐wbp.ۊiA_e' Tր񹉦A{^Y:${Em:=txq48}h!f|Vzl<}r4=sL\@a$-Gm-ʉEhpSGh{SUIi:F[2L؇LѬVto_mx%Qj 8Zj|%G{Xuo͞Vl !¹ eL÷ٹKƗbA%??m&)׷ׯSK'fCcк-^@r1<|:M17: !Q{p6>}#n=)Ddmo#= (ʼnc //Z.vh6ֻx#u/@J谿h{f&${ZF3`(`M$ ɖi{tnAn-`'>7HB 5`f/7d XJc "a+I&lnoDOt݄N)Ry\Bp:H,|]O؈j`L)}Gג>7 d/o9 *`v{ٚ}ٜf{[X{uOMs$E{'#c]jk‰TۄlRR| DxFK2o#! u{ݺ\0RƖB+|r Ցn6eL=eLT )صuVEcwuVнK}~V(C<'G%V[Vz)XǍi8܊bceIC8#f-(eqrm=,fV=5.oR gċv(ɴI&M6*2T_f͝Vwl huov%"mnvqRLCNcm?Jf]5b-FKϗ[q({5["`%VsfA<<_.KH[=4C`fw)8uj>o)R1CddXWMq>KSߝdR+zY$M1XUWR)w3T;g4_nŶgX<\OmW:LR'pNT~Z#ML?1BYK<&vƐh|8,cr GRm\f1V{DEv Qa.zdbQ|AV*M@)m11ZɌ|'sW儿W+x*4v":[Ju59oDl G[†TlO/hHhݕ2Pr}{z&R5-C`ϱp) 6T*=SmQe򉚶˄)X~s?z22cPi4 ?q+3[Ϗ~2Gzɡ=O[|as(/b_n,jRʧ}FkrF(\dRl@4HBn{qdiC?dŃQ\'X܈po{f앇t7v);4ŘeǨpv[i[p{ $Ä{+, KuUGܬv7c3dWHʨ,D6LBkFGNhaIUz1ڄB@2AtP9̎DٽYOožh!D;1~nAjU Ser#)E/lhV©47R95nTvF u;ρQXU_1ىsD6y8Tfd: ^<%{]g>FT>bf<HEݑ/n<A\̄@%ќ >Sgq*Ot@ŋ#Tgvޝs%^FI1X뉖G%u VܿɦeVmJ*Vޟ̤61ϷZd7`Ûq}V[9׈6L /V[ˇ,D2?9u# d_V+[3~Hie!{+łe[~!7S6zpIy }e;mnv#K%FB H +0Ft-w3EH{ nSi@W.)[l@}+c/Wws\){^J&Ha@0zk L͡ '"N#aR \⽢x#2D2)yn2 Ujprt+ZcJ_nڼ8e &)=֥Vx$L0nAFfwhm!yXb8DtT`V,tTdڿR0F#|EƵV{:Bnt{GoF83dkAlħe-8 !4)/eŢᄽuƥV*Օyyirf3TPC_(mxn{֛dS|+iBd{.7q)E)?Zn h~~m+͞Y̽2 /UB[/~G+$ߊ;g*_MsMƣOhjOW˭;g87;B2'$Lr Mx.5`/jwKd '~)p7H4oں'3Un_27z0Uog;OyרR7ec}1BFt[xa;i+ИypPw ێR3GkN6n?'ic9f72@ꣿֻ%C$cH])ŗt6?3Z#7k#[J?153{[ Ԃ5zV<\hjR2zS<t1OK,H,\<"m˷jN L>G]YVT[=U4W ʢ r(TUπ\78^T跌JaV7 'E/Ё,>.2Ƒa1~v$4tm݌J 1l~_mFSǜE" 2vIA ޸'RMFr UBc 5xߊho{׿8LBJp^l/U3;5Bd D_XI:V?H8芅牊=+ Oo իЍi~B:~cAXi)NnMYhK`P&xz_VKxʴ.п.o)h{]t7';X&(`cTW0d{ }ca-#R3{ uh$4K6Hޗ9zrޜ ?ӱx+KId?/?/HF7j]2Q좠 N4gt4 )1&UO.FB;yJ13 Rv^f xE^}YA>US()ЖAH < cNVp=.\cv9)fZ쨖)$@k_*r=a:$F&r5p4D{c-|YAl_8nQ3Rk]h1lhVHv:9.s)ֲ7bY(n{[~#ɓQ`q>՞Wa5O V%LHX!4' xh"S>IgvɉoSS1fYsn1ԵKָrk[ Wڄ^1q6'fo&_u*8Fg.t&QF2+>OhdQ->"\D3YZtC An S,M6/3ƺ vc7uy%;F34UN'zgO?\ PIhZvτܪ۵~IW~yE@-[b.`e=Tȏl4L_Od5!HZ!dft+*IocA4Юځ2RH3E4 2Eq2E'릢g{+ׯgn籖f;It7"x[=ݟ[2^{HOc6:~jt(͉{HCK~B4ޜ")]#c4%s$/+ˊ~I&UXjwcH<@+GҤŀ^fC!'e1+&WWq5i1v!TH&3Q%wvJʭ꒝7Ԃ=UB)aɆ(Z/v|ߛvh~+qM}Y19Pѵ7%wU3nRTb"hupXc j $O)+9 梏Q< Ng ՗Zq+T"( I џ%}i姗)A?ѧ9Zs ~Ce=Z!wv'WSB:LOXRdVpS)w׽݌E+b<:"#dO?؇U~/CscjKnV̰neJR-ةtIqRE]d׍.`/sMuށ,ԝc~8yBOE-:ܥ8QTG ԿRnNRc۪UgK8|~I݌Y:я˜?}^{sNjog{Y1:|i_W_Y>;$"8W/F%/ n 5T% "p(dO TCv;Ad`_>a N1ȡo $e.T2=TJívLbLd҈N㿙Gyt yn uo(&J<#΁G_RwRzhF̝V\{ts!GGE ;Z\J Bˑb,gg+sTf`A"jTm!őƯyrAw ϻ~V_{!gR1v>GS 6)z{fqq M7@Bh+ca{`|Ee A .M|p&,6hfZZ~hnfC"J%5!}],GJػ9Z"R3*j>Tj9MɂKтD}7cpZ]; q)6ea!?CyR{[2m*!07_˘"|޽y)Z)8oﰖn{lW{_6aw͇h4 9,`odo_~_u> F|9F3sg36z^6B 7=LDӰ)0ys; h7ʂ1܍bhdk!($psAM>R!{: w0ۿBBR.2 zu` X]i @Q/tIָyb=d/Ba1%|V w?(tHkIcL]”3&˦Q(ui^Q[щ9ɿw#7:#=ČEh͋)kk‹ȁHuJW7! FK1S>h?(Uu'>) Ɯ3T LqoBk{ocTrT *ULƈ=Tf*#H(VUHTN^4 Oѩ )kJ6ORcJK0*\H;hb-%RZ Z-5F3ᐍ:BP~k=sXvsyy,ԶӦ&z&"(\uVh-H4!U;✘L%*2q&H:o.%vcodBLϖO|Lv$ "& a}#FMM'Ha[ ڑz$ebG VmZ.ՅFq lx(#U` w|5by2(wdLlўnŞjߊ=x79I_<)yx7ZXuT1:|(tH*zy {o̮#AC[1cǺ*c&<ߪ-vgcqr4|_.c4RF+H˭{!&$,x:ȮiFD8[ݿG'嗏US%~]5 6 yOi?l~E'x 1Ȥ8$Nf:4 0'EF; ɇK+n E; f YWtݜAML#j2";^]t +n\;pzP9,lj _qJ̯{P[fDqS6GөƊsBL͔2iuM$8}Tx䧵ПM2PЁh^MiSV!oyƝqF|[+K!*|`؜[AgL۽ ?BH<<'{wFsCO;ƙ# YhZ腹g~A886Zoʃнэd??tHjASp_-RA&^>ZSοҳx{12Zӓǜvn;mJ1DƤ4Dl:ym A4&ȉmyldFRβ/'9]Zjm2űhr?^GY2mValkc,xq["Fm;w"8{=|o%?TD*e'OQBգ8t5WQ**ap=xU`uD(-]"VW!wam-Xgq(G~;}8F2 ku {ⴉ$!9$G95^Kyi0pPe9S.8%oTE͹OLECHLkp7EG$Ajs kY3>k!F0-s'T@ԓw׍* j,4zV,@*XtD';F!go`zAQx~B0ͭb2gQK9/f %?ɾ|`q~g6ԥą^Sr xJ*n}mBփoiD+MZ2A P*-S #Nx #!aFc;99ZSڞ%IR 5#yyF^[  y+E/srd"&rszX`cVq'Z]&B?g\a"~E$!1*b@N_ɜ|G=s¡tX~"*.̀ҙO|/ϡMܚILE0q4"QCGoJZ4sl'"b J]̰)ĕZMkbLpԈE\#eacX"dU!sSnli{Rgk YŬF8u]\/sV{`Z J:]S8yUwk|FBٷ =K(&զ'n0UT#eQWmq6+9!{߮w 5;@Rs,  dYqm>þe+x,8Ҩ CmKNU.6`DI#K]K='(*Dt;)eCߝPl1v?32cM :3C3dJVܼ8_DYl'BF)Ѯ!$f]~%-fn~DdfUg1e?1h$}TVefyoo۩ e夺fDp7{[|yt{/?#wJtHkzAk ڽwwW-". ?)]{omѿ.GiN e*֞)E8áV #s~Q:¨ 1в!M^7!zKm԰xl^UE.Z94e"lFJ|5}Ow74JqşTYp?J湴FDVnLv06!)V5P:-bMD%:׳9jɄfjFGm& @(cщG+~c9fy٫҈py]UF6 Px$vSNk6ES"vU@kw>9*a~1GNFvl-Uxl͒K|οKb)%Al rskid dj%s^ x_IxwW$SaLt1D6Q!xlu?2`G=ِ?,oYH]^z?ۆ{:9HqVj x ؛}`w<2++J h9b? 4a  =aǁ&Okd T#FدfuK@{Ua"ϖqeaOuUkZϛK W>%p~ث}a#>6 JEpLx6=Ԅ^@MqtYw@?nQ,kU~{M!oxH8=eLtbQB{ aFY¯q;:ߕ0Y:K@jv(?~Bq[%o2 m ց+9NmK:E>Z2GpXi4%jY):&֔S˦4;$YHֲC\k'9.F-B{ i\{ɧR$j}+SKkmuef̃+BAg:w4=焥GW)k@@tq#.?*u>P@;0#vP[HƜcĎxךQPMQ̴1 #ϒcQFaNO8}ߊcQ:9mR@zG=kitm6ו*b#] H'&ҹA }sQCSG*\hm `uw%nSA}τxa+(U|'+} />.:]/ xֆ{(yrß!^qG!{X]?|Wǿ;|pgOV;`?7gJÕ&u<} 4CCmq놶$ƀmo󀢖F ̘<1bc2{ѻd~1rYgm?z5]*!cS5= =*2_쭹 .Rҹ( LCHު9E,KSgk J `$R`Y}V'z WreѪR&ԅ"-S㊶ӈ璔BV69A(_:i KmX1@Wmb|@hf1rDMH$GճY)(aK֢ Pqkr%7E[Γ}Wrv_85{LgKls:/?xY I IDATыtЭnF9PO yiLu-^l!uSʢj LU -g_i[y:M3P=z?V]dY^(7Q40-9ġ0ń0{(uR`uwej})__7.)ِ{tޯ!ykU8*EM.zSL JE礬ˆOLT98? 3/s\Ь ^kATP%Ǫ3;S?S.1}T̟T0tZy}¡;jJZmߢyhnְ:=>՚J&Q%Ԕ!й8h IE~hF.sp2 Q -K-rʢ49sLJڇɑX[ngK VHZ?T9ٯ?'[KENG_5`c8vMZ?hzO7j*~{x0DZ2Q$0AʽadYQY7RweS"ֽ1>ϒ [ M#DO9#Nh{?'DjL>P[\E}4\ ͺbL q2`-4QHn!^4IΝ2"9\!\g = :8%18#RۣQRe:{ϙ IFDܷ⻋#1PjFVx_B{N61kބ-E۸ g%>dy_¿5 Pg‹q ).Xkt|l$_m-vx^2 "3Wlf\NSv%f4y6 2GS{1=ɢڇAV|Ocڙ2|Fs6VҜz8!%]>*Ny'>kٺ!7wb Z9H!`Hnae1h%veJӑSNP[ As֯S=-"Z\kөi@"n"YąeiK9n`u=QȌH鄪JˆJbGivpr2`@L}.p߅>i DL-E[grUvXux!fE0%kr$>S@mO {wts p=DLׅSakQMZ`wg.7}ľdˉ Z+)ܷ {YV!^%'hs{#kJΝ<;81r2w٥s/ 7/:J븒.Yzo߿V]L*.mCrێn &9:a/ٟ]S#'+TZzGԯ=Olk[2KMӄGuZ^hN8~}x4(NS= Wv>5֝( $8̈́fKƗswMm&wc4Ux-)pxLŜ.<Ӓ#Rua@y $:K:d67+I}&T  w|;zl4[oZnG5fE{1$ 0pȻ;gKЄv1҉$]?Yq >1fn#F5Zh(- ])3/6V+[XCŒlZ |y#Ѝ]vA0g.QsيBݛ{riMnZJu+!B4EFO>qՁ;* yg.4'ӕtoNT8nj EǖOi7]nge& ^]ɥzVE95/FlUVBkrN2G( d{8MQݬLLhu->qXԗ5JR\s2/B5tT Wi*N< 0?+WdǗ8j9&j|aDHLxg,gG`S6 6!Z:X gLoEu,)Z8`h&ib88F(jk&RW5Qk ulb6at+3tQqPfpQ2/`MEgj6q`N"Li&Wug{qdoiE<ŭ6\c, jv1en:sH5_7lqzjyfhr}MN ~/#s$~]T~#N/kem*!2L&GItP(t}fˠޗ+3h%YUD d)D絡RUM*/BGp6\ %\APyлs8YtPq*u2nԘڠD^eZ3+N62w_*;;VenFM@aŋ/.\*EHԘbc{@D Ŋ'{c@C5alZ15(`"AN7Y'n+_#|րHW{w^euB농+w:8{up{mi|PH1Xڪh'Ұ&*mMv(>5{j5/9L{2D:tAGkJD*nta*yAv޽ zc5$:@ph%^J8$9Xreb| Cjb֭47͢Ot 里4<,ׂ!A2AAeͳŒ)S-eq:9`m1R #wvZZ:[m8&yr E߃ǤM 0񃋸eճR>RE B@bRjFËY5u6ܴyih;.S5$CZGiTP,jHYb-| Nr (x%%ۥ"ÔXƂcp#L+M^]:8Vslv>R:SQ)Yn{Ο2H 5АS%&/nt+J\룻z6Tt&e/R uK4fXD=K;§^aJt`|o3K!9ȣ6,!PP,Y9eK>!V[aSm-H]1b6zX&s}@jAT|HN`(SdcӼmOsw猟n#lEK4Mtqprw |ESéhprzKF]į3h+&e EVaqљn;`NmHF)][# :lid?:yMqM[9Eb&NQUNRLc<;b]8L~.Cq"Xh,'pUo,xZGNOԐúH]_fձw(Ӑ1t 9T1 %qwX!O,y KMKmmX?4 .~` |V36%+ZѧƈB`Gh֐x`Lƅj~ȼzG *s3Pс y]E<)6QgLuo(,^SثWw>֊G2WY1F7HI!N*„y8O|ܰw^rI):Vz.)+3cOK S̈)Vj~??U&!t7пLffh\+1X [v}Y+1i p20gq(tbXX!DLa tۤ>mbͷ1[Q8w_k&cEM-]JZMK~pa{">0.5}(i{=hJ/ko^0%3.ROѽE\&vw* $C_SMä h 3Sv1ȊO8Sfwg ѝgRnXBH׃/kEܪk~qpa v;ƒB80z0ȪJ8,Ӭ<\ XhXK]iNSӢ9'gՂZHbM(r=FMyHdB3N$cH20i qs2f3cB4 >]kCEz敠p*Vl jm OK}kx}̸t,A]r}b*vhlڬ !?YP&"_c=7瀳nT&Dbm /&ye{6 z=H9@ߩ- b}of§D -L}c׎LV_K(MV)>@œ?ftjhHaz(?-pbv7s(a@g[G7يCQo{w)i§؝)?TU̦3~7|W\rtlS:~d7IiN,Qk6|1[jtHp(2sB̗.&dk'OtLIA߈}d!m>z[0ҰčXZ'$ Z^}&mk7SvZ=P.s=Vl&z/JޑK=0n)y_8RƳA`< QH"T*<C,pmyGes#k;R _(p H+D7$ׅhX_ܗ ^D.]Ak28*?O48l%2<:Y5-P`ms(eQxmk )Ƒ>@!2d:X!0E]r[qºdsbkГbe-F}.ҳUeZj:bۺ_&y" IDATs)b}hѸSAQ 0)B&ĂpCT2R l cwM4\_*vN C9f2Mbl ^/GZy:[9faRI"N4谓e#NGʁ84{ Ua<&HPK3I'')I#jDaQmj7wZlcץD*9W&y 6Mkn{ʬLP25K? h:<{na*q>εw V:ؤɴzN0 9ybj }or7Orb/m-|wɃIրb˜]Y 7pjAB{S%G|9OvH:{*6M#6}/ƥv42Cٰg:\C:;N[7HmEն70(έ)%;n@qg/3#G]5MaUDs42Qd,p3Fp[kr/V<Re3LGvZj?Zk~~DNa#vܹƙ04 $.*¤=ECTu\BnIWWT޷L6k[>ڪC0N,;4wQ4')2Uyd]%x~bCǕdӠ_n4!ݵ .V!M 980N@!/4m5wMfҘstx*Vb3T[EKmT[lSz%<JLqW6R.]p^ւ2?,׶774-_SeSdPwEAOӔ{ 3`ɺ 1}Z? tj3QRm|don Ar 3?,F4lT-ܿ4 @?oEPv#:QBui7 -X!Xfj0`:א:m (V&^)xTF;8" ȍQN VjYNUhK^UjuսjEQNbk'} ?͑\/ѨҘLiث0zpUӟdQS4=d(Vͧ{#&AmGuk>=LtI@iE)S\#ڹ+(1/jxrt/e߆p<` VZ&bcBS5c@5I]N[#6SzJkw>*[O9BGoũhz9ku5[Xy@<]&ſ$"~g}WQ)G_R `+v԰i)$\3_& 1g)H:͞8L1 Q.0  qY݊3ݏǜZõt䘐efr,$\7mm܊yI|;Udj֩[Xo;'`{f8jUdm-=B/~bSbhX4\w A$ESm,dS#HhMi8^Sɦ/ku=N0GE!>ŴSo4Ј"PPsF4_m݅u`\#'P&4-8pT{7xuTkȳdo{t&,CaAs4=}of ݜGą 20\,WVs{*ѭ G1V'!qv/{(aSlaʴ(/A)B1Hz@kT9{cs1E62XB;!{^ȓ+9:NអWݭ PVU8ES.^ B(hC^]mq߆n,㙴~Vl]Tkw1]E9BvNg_bJe%AEXA){ix. ×ğaUTSx߭멎^P46FLJ-͇=VE{)`-{FHgEvlK C^"{A^_6 r\iZS4dd} ~ne@N9D:a9Oa1NXo5V8d FN 1TrN14rjoƕFpeN6X\;8uD2uQL+N{(XiMc"Ê8v}Ks&Fyz҉])h̻,]c+N*2/v{uN 6W14iћɑ-j[`Y"MyK#n|:B+^VsN>lrWZǗC!"#4 5r+/Lq: u6ײ 6<6)6xd;5?*skkM48VťmoRJVB"Ag̸EW"t)7e96ܘ6[&g[˨Q4iނN#]aq .X)r/SBjD4FiIXa/ǻ:D]^x,<$3O[uk L3<ݧnR<\Ό]cﻧHbe#M;bD&⴮A=L7Y#xtX"ֱH~TS"az_`p4%e9plDfݎ+!D0msv_diHb#AB誘TBG$uLcoh\,2%_N:NT+Xx3zci{f)uN$}w$hdapmF\@߇E46,r):>cb!3wLG iURjJpjq"01"V?:S ]%jjq&/ku^=>}0\@)Ӝ,?E)`7g+]f[> Lw2kf6KnH|PL^P^S#'QGLKkvѾoV9#p|E5j8\upѧ>uX\N#B tO9 /cVʭؽ Z؇m>C؝9ssGiT2WWV evOB3nb9ڍF %/t3O ,Tu{D%$hrSh dzJ 9VweN /4>,)2E\ɥJ0V[mֽc>.cH9yd~R~5ޛ 6jÝB sBMr{iV輬>KJdPk8Jumd b0Rd`h=kS?O6Iֽ|S A~h emn*z#4!O5 >9&ht]hbpfS1f(Yq[foi!O^;/( W=Q!_Wz/AH/|8P9rBbXIO;+M+P5gQJeW+jMAqESGu'DntSCǨI+3 >6uv$N9^UXƶw#T&U|ZWmVt-9dNZ!m[÷{Pdezgà)35FsJ\̿ _'j|CX/kۣqLt& ?˒$ 㭑2'{τ}xIGsc-K0hݚде*9V9sۤe;CqKN(UQO/oR Xɭ1LjɦfR;^ib!B?9TvLtήuҔ(#)vjSu#2+RĔlQMF:_CǙL2׼E}\{Jv(:_6:';WܴeLE:xVVVe:\K6͉S;2WM1fQ:E\1d$q?竌ҼU)1b.::嵷fl(P)O;(pp,}cj0Hkkx#eBe͌D YQ1iIdbثJ%s ؤ{=4 LHnLj%oMK9L@a6 >Z]X9Jf!zRX\Z||Ԛ1l>߽ >ZU~kӄוze4!;B<:4o;RP$'n 6|] Ygn8{M/YwjZjF^v6?X]'<#&@z@o&:秛i>/Ƀlut';67'2m48/әSO0x硉 |M+ (F]:QÐrUr,NZ?Ykl4t}98Sr&fJ9Gӌuoo9+3ŀX3'cpr>D>&]tf씒8Fŭbeѳz EM1 Niې|0D tDۨw Ӑg,V;$kx'gƸ\&n?t@J_+;RQW:uP.J!mX.P,v9Xk־5tg/`] 4]ZÉXDи."^Iݪ۪b8vh8!p)WӮs L VJ.16iθ+>K5~WTSF(ĎE)sh/PASzE\\?TQ[!*Q$l_T_YdE8';Ґj},Qŭ:ۧ9x&'DK㬱v(8EđpUMjC6.೴ [w35«AgJ`#!b\ Z$ iݑ6 eVu}-ˡi%(=ZV3XS43${Ÿaܲ9ykleX{b]slV}U9=n $/Am{Cd &+t|Y+$"Cl sȝ10FӐnu-X;+ڄQFgD[{(BFWGgQ7%(S tiU%9FebHSLCj *LFp؁qy^)1"Ϧ⟿m~s;G1e-0Bap qaM%tiZaB@o#QAwk,d!^{*> 7[8rfmJNCS)`y2?pMm.z{T|:o6~;&}P]n[?} NGlJ~Y4v__7ŴwV.g ޛ8OpyJ-);^kGߑ352 `8+: %9Ž )טc/4ڽ:D)%{7]H2e"LdQ + IDATRDχDerF8I9j\HqṠԆm7aLGYm S ёTBȓUL3ࠝ$GPj}KWMdvCDj-.yIB\=^B ))\2Gv U1?ƭ[[mQIH6hh3lUN0n):b¡+g$P?Q3(QE8)̌pYk֬!!kvDL;DŊiUjV1%h)@ '0ɥ9E. 01)D_Ά1jwu%RV񼌬ѷm5uϿ;\0m L?4v_$ʹB$w Q?t.M(||% ?m"p!Jw.3V.@f s{.y{[!6LϟmCٻHyUȋQôK0V}Ixu7?b:M@ ي?/;GI6s-i>f]( z pRd3|z ! {K0S= $@k@!,k4Iv_ R9BQ:!tK$GΖ^1fgEY)ԁfTªPML51|utuo5)Aƍ~ \&?\&guNy8ua,sdصrT0VRN1az -\ QS xΩVҁJG0.qk |GtVwt0dﯭ\tw9Bs>*-*NƴH@ ,Q~TB9#RJPjX|:eg)=*ВmkŒV; a/,f$lɧsaJk4kko'_>& L(BDW dܶqYgQwAELYz;Q*YjZwqA'{댊ؘ,` ƍ1FB hmZ čZt&N ث 鈨' X\k2'4)G^9(2壋5 g(R7(@uSK f!DM/2{ #-(Bu8OcnN /`_jĵ9VF!ln칏imXֹA/3>Z-+WR^fD.ew" `?)OB0\v|"Å9#5M)FRC tMvm>RR/S2'.}wJZXwq"֭>+,:41.@\SD$K|8ej;DҹSΊU!8B5M,>!LZ"Nf2I _2nNI% Y!_d0z/4Y#zg:w<,e-r)nSخ3%ҊZ996AÀC0Չ}rGÔ/g.)`Ѷa|eۣӉahCLBb4驵A0, PȠH&˜vkXKw Ww-Spgb޽֣+:è#4$_r^1Aţ4C-m9mc5ޜl_Ɉ||h8=/RwI%2aƗ)mSWѽ # ('+7> ub)2LEܺrɵF*Pk,}TOdCpSjY1Cbo ^$d"%GQa0M&F$S8rK%]54\qYjR?I/\WJ' \w#5hVòN7Gb">eeyW-\ e]0Ů/Ts(z`zsV+8 ĢO9סuL}L w SxZFG0\9!fBei6vLӖoH=0P :E{à 9ao^l}U_OZnnuen瓁7T5o80`&g&XN&uEf~'2)mT[/٪ݻL+#{vg9~l|0>f!Ou*[?< qBK4KQ Ve y?GyJKRncCV8G-k-CrϳTo[/wV'ZQ?8z(~{T|w7`!]VrגZwZXA'5Ի Ԑ,,fdU,=9o;Fyz<9*c\׆Sϣ=_|poyo6z|I촓h>ӅvQE ,xv 0'Ӟbpg 9 8׃~Ww\{JL+o8s1k3ܽkw y1UVq8B/srAehtIԹ.%H0B[Iz{4euZ\tf_ef780[ ^&+|6{9(Ed%$bo*Qq))Y's٤7%xxxb@4\cթ{u8PG=K0h[I[#C>/-c 4{s&w)O\<4ځ~wCbnkn4S<<Ҫ1$R) *: Q1OSdRinun6 ΍0 MQ ֮∩z! F^ j;ۺCj qbXi[uTHcYi&>Sid~Qs^]}=rNXӂ//-<Қ_b[,Upɓ@|<%'Y)yv8D׽;OO ˀ3NH`ށ{onP$IU}cʤ8hvh\ɽ6iX@[ 6F tOAKȩsNh0VP\qJ!@;ٚ<_)aY;X'{:cO'^捷C 7;N3z?d\pUQcnJglW{S~-r3!W 6Y#wq^&uPt [<:Qo1bP9<3xÏ:b08k8rg4Ǫ0 )|DYcV;cԽؤ-~T^%krtw'?陋e4\ѽ k\E}3Zi1?-5B;X&}  {6ŕ 95sc De b<1 =O<8[Ӝ-[zRI%Tsbi2t:C4öN~];:%꼘v@vc̶on?V%8x#P7uFV;~~]"NWfR1CRD~SIV>)&٪? \/wSmXR/{ rl9ي;jniBcVDL{1ԍ:xLe2!*p" l.7l 6:ޯ_xqpFVף6g)KtN\&πiFkz0x{/T[qaB3ʕ9 .ؠ2reR:j8%cҹ{8ovmoqؼxF7m`ⲙLY8e[[/gC}oL1xW%0$^FIDiꦬӂ \+`?{D&o7}td0_'<&QG7|xo͢jGX(<>ѽn.42ȈӧPj6ֱLoG\&.f$OZڀ$oq \>5dn6LR>+kfn?6;j3qoLd<)#y@ ܊-*5y ^jU!H1/0OO߅eY7T0 Sk%+":6#TuXvg[@_pIe]0}=&ecq>ke =Jsdhգv\XΛGưkAM|F *z}C9؀{s =;/Z[wѰܐgf%sU.u~rkG JiF x ÄqQgWo١fv?6nӅ83fAz{pq谽}Fw)^eMwlu'm7%mdn6dQj}y$,1Z%_M3R[:9Mlc]YYW. o4ܘ2_\>fgOO؋W|G6B=zTwxEc;Sn֬Ⱦ,.ѹnΝHt/;_JiRIzrPm7)M`3/=w-;`0すq!*ZYNsi/˜b0b UľçK=,}e:㇋u~q~4B)[u/*q>e^B:Z2∱ځCAG\l~tѭRmsޜGOF~(N6Jjܮv ?8kCMnL=ԚکYR$Xz9Yؑu_ ]13js.\3#°)GKδ6(#iUsSP(Usz35cH2 QpJc"0N#WPiokL?>gH<C/ڱכnN6~M8]Qw%c9:80b"j|N#"7E?f{@G_ 6\O0uNlVN*[ AC+}͖hpP]Qңu:UX@djWB44]]~8%v+!d7uDW<Ŋb]g8NK5K>M`↱?_C1c0m 0]Dk~߇49.:V|ZkDi1߶qOj4Gk}݀B6wwhcGmJ4[mJQ3Cx+kF\}ҡ_VlFGv׎Ťt`~m2^^:ԡх}YL.uDmD} ΗkAC>v>Q>315Rnř[nqN?>eevUd ?<%k4Il߮KS󳍳K2Ӝ ]<o#Êmß_6/cࣔLƪ_TirTH V\(&o ւ%t"7Isiҷkǰ){Bx wOȃdtP0vt78`0S=oT'Ҭ5ñvD =~K2q-ParN/ÿ |LFτH _7Ռ"o*^\%%3Dz LG>j#Y`щ]ls7-/2ਣ>Cvo@#4zLV9u ÍHW{, W3HVLi].c4m\':nnہL=6*T2CżH~TAϵBͽv+R֙AwTh%:*JJD-9r xb[w:}w1Cc>ʁlS `USgVaQD7](وոI>B.՟/HK(|P3,nI/#e]KKA{#YYnw2_)zN9eA&Lwy3ӤM◒85k{Nj? f#LG/vv|-~[srJvo0U}z^lm MŨֶ#ax SZCp>G 84?sD&^Xx a ܊iP( wi֨Z,hPF<74ŰrpK~L*԰I)A?8ZdA/f:npT\ro+"H :H9>ḈNٺEr}#ex8DwO*JM(~YF?t$^4jbZCȐBxcQh>ᦶ޻eً;u.Sk'!A'v^ʼn]՝p UEXqޭt٪hp_I?uΖdS&dȧONDAu;'ΫQ'8)h0"XN֦`1)W8ޏRi R%9~.a}Jn`uYlȎ<k Q0Y`Q+o 9*.KHiN8^'7+K{ܱ}mF޷Wٌ7RUlȵt"A0E<ڏϗNN4o{#{)Lxˉ|*9u+{)s<x@_fw IDATj=S'$ύZWvxZ̻WwI: ΋Jgn{s, %ꐏ/qDWyqf ~+Jfp)=>UvjöUw.leqZy?~|Jwf,1Cg'v%tRjy}H[XOJ3P <@YxN8##Ѓ%Z0,}_n"jmbC6 DCr=^9:/U(mcQ<u 2k:-R#6F T\d zSdqdb]M'=t*b6agcixщH~Gx%*-hεSp?KpdqC(g׎:9ZQ0pwwR{];v/X!{M4ܼ=?0/9R#yzO)1jugxQm,ǀE?TŸ8~ƊOcT&1*lar@۰S@*B92ipS{/'>մn!`칍x$iRZ:uk*lYpy N..9bQndj.mᦚӔL1VYSt]^k@ݝ8  U:xwJ3)^Ʌby).wjZ58,|$4ew )[qܔk TG撍[9&ě|Ji$0`9,Q'[Pl|/A3n}]f+o Nw}(5"1Ϋ=4d,[{`x\_ JDO3^9pB9p^Ky|oޫwN3t09RFEI%(MeTG-;T8KH~)[.S^*a`*};s۸+SF-ߊ}k8.T{3qՌ y і.#NɹdK@L?I`J0KAIK)R@2h,ahɐ$mPXݬ#ݴ 09gG|6wO)'|ߛwpS"M~t4!I)$ߨhdu^& .ꮿջLG0q(wS3Ml]k1^ Uut/j ,X(7|p^"<;c9oX Fa Mu;S;nMmfBMԿ>>}-5lT46L +?o|@bƿnNC#u .wc1zFw2YA7nP(n A'JIvsE,S$wmHmf\ U,Yܦ"ey7NAvk3לU"J^(jgCa]a*t`*&E:Tў|Apt9iѻ3Ӵ̡汃UGGf,B|^q{r6zF1Rѧns (_Tv v|a'&Lܭ7vt49JG^V:e{cZv;IBЩ4ֽ݋,K𷫍֔.18-IESف%;VTY~~-2Zŀiꖷ)^͙FaljIHC=/1 bTʬHBk== 1U9۽~WQ`? 䄭(,ZpddISDI"xv,|1fLʤ:)"be=7H='JDn{u߭p%X SpKYtpyɐ{!YHˤ)HE ҤҼuܣe,\5!LH)!j!~%b6JN<;]Nd'NӚ"P"(8,ɻzWז~SKًqV!x.C.:̹ 7@uD+~ES[4:t3Ė#.Bh?byn7Z3Ç؄Q gAmE~5lG  giWC hQZ͝4Sm0 5>N(=gYFJX>'x$sM,ѕ~|ʾ'HT>&3jkSPc#{Zp.s(q^J9kYҬӧ3q9F,+ ڭ{k#rcX&$GP^%iԠ.{Y;/oe`|?e&lNjD+؝ tKXߊ')LK;ڑOE!t+]NɾQ·"aI@]]F-$eMx9'ցZuf>Qq#C#ְS~!1p`'_t~a(mڤ<&(>Jiz@E5uUn^ŵVoێOk8-ҢqMkg+=8n6m[P2q7ba-`ywapkQ֔:8 v麗a#sSbvY5G^1Ǜ;A̿z7u(^[8/cÞOO9bx2Kr R;;_+W+GExNU Ԛ"-MNV:iz^zO|pNBū{۬R4]9F<ԉu+x[`AdQ` N+/VFS[Anވ89)'b`=Є&?//qh,.1kVBw}J0X㸸f^jz RGG9[wpuD}?<%ĥYs_K}s=o;ySv-f?ݷPm$$YE˟pV=W` @VhCEΡIc~cQy$F爢pp: t&p@tHت\s`jN6 >tr/|Xr"ݺ{N8r#(j;#FbG$'LP<:@\)yfs\#>P&QrN-%BwN>\nv X›)w,aQj,ſ{*v{_ἸT`':t66z&PLkF Uk<.rdi^i޶M' W3Zvf3%$.qZBި`;,;Gİ;ܿ*\cѯArZ!/I xV*C,cL_\nR355)d5C@~ V^: \/pօ)RF<vnpO!]bc"g+Rxxmoy??r^r}tP 4^ fnm2? zd^l#ԅn29fSpJW)tKGQ]Z@>N:m8pr9>دhW[Gʁ :J9U3Bcu0I󚼡)JZAxdbBE )'%+ԝZ8AuɸĀzMPa~ϧeR%sv$ /|ߛG~{tY 5g{Hzk/Âb٬Cj:%DJuly WC/Ò{f+p]ҡXO74K؍{M6cݹ_meTgo4C)(>>zNGkheŋ /9yFTc4#-GM7U&z}fY19@6rkLC8Bqnu*&Q _[F0Q;hۜ Wb49$ oc]2p:`1,~0cJDBw3ZI`]Qt!TښfV:Gˊ就IYPEv&J8G::+i=/DT^p]^VWr}kM]2}v9q; L@M:jg)*ʝ&Sݒ\c{+|iI'tBĘy H 6z=}xhƹu,qq%O&W=(F{1Q_Jxp7KOȦՅkI{Z59ѽ-0Sf*uv95!4)sUp;{ /sKb:MUqb0E9?ՁK.c#Vq+d6]紝8k9gloofyu)p>WOR mG~*e aSSs=l,iaG S&th v̛ %"94/8i ΅5cP9>#sHJ0TWr eNG<*<V[t4]ObS*G L]?4ෑxU-;BKꉀ_`Dxgu;O1`C@-pL8!7hn1 s#-my?Ja/|f~ 5f.8ʉbiwt禖G%{љS1NbrRd[e', <"khn *>,I t3׍K]+ S.n'6ZwuhĕğbЭ1 fvIOH{%XN6=u~k}bEZ TG6ڼJTIxG×[abĿdrx~4^HAKzxߩL)WRˊT/),1uk`X>^G8Q}sKm S@!=114/̼˜ڸ tTZh 6t`+`E-" V/@ujdRYǣqnsWexx9(s ?Eު ,+E֡[Rbv6.IrO5f/Zhd{ /l͜'~ٗf.8GE}Dp4[r lb5mRM^\c|/]8!{Nρ\9*+CF"YGVHCG+32Lyv)/*~5BuD25jI X7_Q"iښ9Jb͍CI¬1:`8z,-@ j!о-Gҍܨq؏Q*"M49Lz ugn.qBOa Q5!:6d~O_ϗŷ2_D顝P= ʽ㇋+Gki>y05s'wbo+m 0g}wcl8:RT/#ؼKn]@m;ZGp-ha)=,|΋Yȏ`*nd-){dctHvm 鍓C8LC@{ jp|[# aDbCuRVmCվ(j@jhp.U0@gNLV$g1F!0ɟ3_7g ؚLZhs>:U v{,A^ּry/|8[1j+*3۔v"m.5~[yHޚ$-0T6 Am6X^ٕYFc~fg'^Vi }m):M.* 4:ML+b5zcEpejClY!`'!Ȩz)(#>O[dr?,r/_nP"\ 9#jkv c j>_;\BϑY[$R8NcR>2*A_r?>e|<[oQYֽ@|/3ĝZ(f xND Ud}_ ^I+< 37>ljx}2J` `'9M{A*hSt}# Q`99F=&,u"} ZBu[8-n Gi]o,e/dڨьmrJE9qFL*t/]jVnb}znۄ7L@zMv>B%ol)4|X-~8O2xrG$kȄ({>;`ߏuSDsHޗdc^3Fv!q)xQ%_GK0(}nxdURA}Z-=*ep匤Y6S|M eqxrA#jBsO.7Mae aW2ѣKo52jpJx9'^GQcWWd3Z[Ǟ"^Nf@x/۵8m;.)Pk3Fk(dK#[idky"@ŽدY-b*|XRė[JYO(ITjG@jw|A[ Q)NqMyZ# $BzkPc1/?d:.iPng)#`jon<0 (.GI3Թw\8Tt |*"zoy-V{PR I$!K@݉ ?lI ]x*8,~%b|d F%<]k u{̓k?_ Z|tUTJ 2v1|H}@:,ءHNz)f|2[J|HtVHnvبsuVv9]GչaZ1]֤-1\ڱh|@<6J3:9g۽3E8 3 dZwנ \u,< 3"sd /&Nj_94c/O틯w3\xȧ8?X׭Q8=g:ŠzDut{'m>vC9ŭ9Y^ϑ#3o%݋eNpaBީ%4,Cb@m$ `AF[UH8$8s{t JikMZ1k99{_EQ;jHCZxA !z]D|$qK;o6L]΋&/pBL\l/,L}lRS|#.P=:g" v;RG UzAU(;:P1z xJ ó.Mh#>1:eGf†O,xBxbl/rѪcH#Hy@/F;-:vx !K Yt)LV񷷃yfGY!iVN )orE.zYhlib Su;j?[bŨ͉]?C,Jmp0P _ nAN( ;p$=IO@aǵۍih@N<3 6ud]2 PZ[%ٹ+*JmxY渪PZSuǚ_K9{ף{!Q&j$Qg# 㳢 r1Vvԣ)eMqԎz;*b 'w/!Zlp4+>_uBo[?xe%$bn wTjB+"xj]:4(8a+[@UZǯՋ~}?س_&;%q#}*%1yM!a-S βKKFm&` x0{sQʔªT xL3o7>9VS+YS- P*$o,r ^.}πX0banIN[1Kh"֊_GIMS,IOX5 po7}J! e[k/HM GpNKw,=zOdvM/|n.dz<爗 @܊CEehRt 0xeڱ{`|pMG 7߉gF'PFI/`]وB (d@buwkk!vf<[wˊ=]p;*B~6Y!zC7)#0qi*ml5vX+ZSQZh^z1%b6rь@Prf!gggSwm8E8}+7Nn*fG,B_IQHY*j2]k xX?K|9 5NGa/SK7€i+|Y[;|PҢPyI܁Qqm Kms.و-v~'[q2?, [n[1Uj4O )uWj$`vS߶WF~"lusM_I=:{%vWNVe*Z#!:"J;YŤO!PǺ wP+rߡ i}V$Vƕl8NM攰VU@u?|!Ķ 4Ӻ<]+!x4QVbHb0a6@K3M+sO] zk?lțpN]!}+!v6fYj!=[|1M튉?k3#xU;mkλ:ؙ{^#~ V|§_v$l,! sM1Xf7ک'ft;oX"˅U [PqBq q[0:~:%*FoS/_C C1"m v Ύ@М]#߬Bw"]u3#߲;t1[#M.2TCU-N͵u:JwlJ9n#W霼K\Dl$<,sԡ{O<#{i)bԱP7@uD͐6uf4!I67I9Ȟ р֫L]T<ӊ:Zb$Մ9& B w_3EeaDYws)gK4>wCCq.Ld0uI#+O3/]Hk XsD|JqYuJho@`o+%&k/d2a7$\<=2\YƂm_niݍ84uXumaрP\iv܊;0*oBe@.P誵p1>bEuj@#ݙ>#su3,uhf#QZM^N bٽz3.L[5M:ldv6~?,VBb#_䅽~V2Qs6>LV-W@f:%\1:^^+ )~+ϫ :c`Yj`= P])q)cT]m끟_`*eqMn2ab9j74٠$#&GhiaIT0#mk }+KD!$15/1J0bmš1=m~1NkgҎ5\~h>znw*VcPX^w)i +qu(\EVelZ r@ô5kfݺ>5h$~alqu8utkC'q_(hxH]p7/y{m8{+St߲:[zH!aĝn}D}Q?6:rL |gfgpttv"ݐ!ϐK#Ii$p^pڑQ4NunlkF)YA&~ڝ9=Rz6M%ϢJw\-vT+1*F|1xb'jc1T\}8YLǞ gt_r $6&tgyiyi/}~|Az΀wvU+En 6LFȳ\~۪\o_g^}Ԉm#(9'|f8'75hljc5V.po~,x?*"ǀ߮F58~4쭳ѻ6>{uh{jXsF$#Si{wسp4yY&EH$O68i9QY99ZiEV&ޙ N9uq !48ZG/p}@fKMFt"}xc,⿁ɕ`7J'@N憮iv6Kh#km޴>L-a 8ʦRݐNnU$rjIj=8Yos{^vSEHrLv ǝu88Lg)2 W n>. \vv }񿐍gA\.!zᯱvk9 !+C=_mfٿ,3Nw_KoE]I;4$rNC/ñyqY"~y;E}<%8|d?2>CrYx"`^h ),|:'Ϛ]S^{9=dc^"^]l%Dq?8y^/Ǝw@)lݕ_w!'FK__wN!O&EvSFʕg1`^78d1 rSqKB~(R|įi־L+e(k ߻Xш5ݣ.Yԍy) ދg14n*:3Vu;yJ,{/&pN7qfw|6<%& [&\ik7;% (JFNHco+#u` ȽMMF=^p>B~xZo;EͻVrDvQg*\eBsl`}mt3GGZ;s`cℇO&(N9X#u֫MLzN4[ط:~!BozV01{u7 9P9YmSw+r 0en nt|ACT ."Jr!&~c@d FC IDAT}L+B ǥ ΫNX&UЇd:。N~ܓ'& !֣l1A}qs!j;׋{9yN$ؑzY7(?L _~|ԢM`s1ikȓ@Ak 1-N#Q%M0}=[NvXI%k Fd%FBx91sޝ-r7N,d E9 "2Xc&P`)gVZioτ><Ϭ'2O޶)lmca8#hCƺVB%GY2sS^NSIw^"~4m`Fsڙ|a˭<# `mu~V1"'۪g CPXMZ򾜢kT鶤IqNCw@x(5!%)9;ֿ5Qq7 2b2#{j6($?SiDDWj2`Gu dJ2# wϝNN΋ KXtE&:RBbyp׆lS^ޫ4q'}v12;YR#nX9r׆Zq{״\gu4q_N{vI` };rQ?FO,$GCVm/"Sҙ9FR"kS<~~xyq:3IJ*c+5k|eh\*n,dYk{Fx>F{a ]>s}Y,UEf:1?>-˂/W -,h0Ȃ6]CbUcZ&qYC熤 ҏ6FƱ̈SpLz)RDhԐ];: e ʽUf-i6rWWvk,D mq\CHѬ+旹dcZѵN)GϠ815pP}GhhX|1Yd۵4_jCphq63 Ykh:4)P _. 5G~-F*~#Koq.\!}1[ivOMai׏Z13N<83qE_?6tK$7JgG5%41z1yNg ]9xO7aIًtQ{?LuOK[u=|˼ΧOum D!,N(ҵʣw3rԎDy!dvφ&}lmy^隟[޷bu輌.M!Y8KxgΕBU:ssud9.|oIS+M}"X7LS1ķ^u^̍ PX"u,vuwunDtk-veZI.1>$vͣ`]Zř~-zޢJ'}7}@%O9}92ϞuJ# ^"FzFZ3XntF[Ct; u϶NNqNӐr0RSA2l);XÊ5 dU8P'0 ;'$o䅭*CeNҖ ұ O۽z9G5%4Zҥik>U?\u*^_%YXc@bQ?3 \v65ֆ'9ZwEL|\F>W_}|쌼Ԫ*O' xY3R`XIAЇ#xY$h wp?LAg3&h3uB?=>,ifܴ6c ۝M9| lfo4Xq_5[݊?,#rAŽ*_=lۇ~52#S֣r}(睰s1@ݑI&KSx/lo^d$s&24Y;F"fviA>Ya"+\_QASlvhljZ:эd&r^iCE] =Y}1uB7$rJ,)P:%頙~RX ōn1[|ǔJh0W%50N AA#?eqkCuT Z2Y/Z&;`qp[ 9,' uDr!d 8 !D~;0:u9Ţib/#2Ɲ)XE0#VL""UI1+4aen}$w6/;O¤zllLp h&xEqzۦ[śs69:*k4ϰD 'Wq4oGuvጪ{^gHٙ)FZMϵ5Y[{ ߍxZ^`Q3b^np/s;`G_| ?]26!I)Whs̓l/f08b7go<'(*]+5:^<yF!x hfƋdC)}J#ڐ#Q#dV A23V(;ucX8 יf<IzfEۖcK٢B܏Sy\JIF?*Lkm4I_VN 5 JRt/fFuIK u?M߯n %)s2p'j@CSS_ lyOjMnkuP _Cs^OI2a]+FLZ*sl8L޼aLeN)_ ъ#yB>+Aks֔\0xJ&DOpIF`iLhn|Mz~P\2a3dwkdX8jG8O.Z=TBIЯxT\in%|Nx`뽐1MDFmo[/ XN!80~ N7 K R,2DZ/1PDVqL/HL!${|S^bewqZ`\/̈́cոP_J;ՎwSůb]詹Ÿ#Q !m0r+ =?Y|'9hY&-Ƨ($i"Me|EF̧+h;i=B@ mIlNQ!:"qcƗIhoN8M"2{OŴF6#59ѹ1r6me%Й$h]sޙbĘ +ؤu(}\$CܱsF4dl J63hUvpE(wL&]NުK_9ZFӪs'%4WO:S83leC% 93A(/mUO L 0<]F"?";'+KMES-24.@CYtb$%.nzL ! !:MlJjE)tke,8[K*Ck+\<^ћ%/9(;^ScdKͫ) 뚐z.EŏBHU=)wϤQ:8ohxpĔ4]ZJ|LӹS?][>m@8ٛkmK7ݎJoe7 Zki;v؋\w6 i?-f5cG5Bk/ݎ1 P2tV3\^{Y#TLN:ffiάprPˡ5j$۫G8$fuzMDY?h}oiu!H -acµ_dZ%_%&wf'1n$D[A × jSOe+uӠw3CJm90vGLKf*tʤK=`yeϸ1ofE(N\;r?aBA=S>! N"k2]0Nd !9 A˜IMxf]V$;[il ~+)&]Ñ'Ϛx!ȩtL&{wʀ=]Z]4N]Vȹ{ @eiimgDd]ցݝWwSPL] 4c КXWpz\ў9:ѵągB 2TL}t"Li98Wũ.Lؘë o"ctKB|tإh1K ]fS.u0sz;Bx+טGJq<$#&+BX5TWΌq3N߫0ۤ}pb' xlrVgQZ e1ŖA#kI^z~frٞXd+?\!?%V9mRS < %Gr~x%u)!΢~Hwd[c~7 i͉)PukJ#Xf?GG߯,)>*& Y"J(`'F"1öGw,5Bά( #V\g: 6c hTBX'wXcJ&IoUuoʽ1& Peq. :~;j9Z '~<8pƔhˬ~NZ}/֍'8a@#,UI% Ws s L׮Y{=:ʃQr⡶~;۟kƣ4|0bxŞ);r3 .QBhqgꆁc l-+~*e gJ*'C;Ӹ>;%U |Ů~G{ R5œc.nbv?{&fϾR*,wQرU$7܏L IDAT) tqEZ+.t1v`%,$P)}'E֙ӶW(>勁krk {D^&q?sZdb1;&9a4 9jbi"^R4.JiR6!5Jq&R[/EV6Cp;,r6Jǧh]uMfҟO$&ѲMghhG8 YS'Bu:>],(kJ&:[!7ژ;i1 !&7KBs V1Aw-O.~;n{JtqY>}#Pdx kfjuo晩 ?Yo73KXtYVh2o[uhu"lF=pDe6_Ko|w1p%&KN6d̓B~S*S1Zw˚3?^}+O>hȵubA{Se} LQἍ>۹nUrsɴ/7L$d "צ =:g-Gc߻H#~)nąSPO :[9Be.L|fO Uq kgASoB-,|.tԫ2fNq偑F]K4b˨#M׻O1hk?j U0ʕ[19!7NJE;* vd20೓O߳9_fW4s2!qFr@Å57Mp֖: rU:MY'#4./2TRh<5uImV065hGv!7fukSdl:G·4;7ՄNt (]Bg^9|l,lݝCÒ ?!<Z5*ݴ~?0IGah͵SNI8v'7T:jnszVOsXŦt+E$ X.ty>Xcvѳ5IGk=Cq{;` !;=;mZg"5wzߤ Dp5"jy=| 8Z&ଵ9x&'sP+u+xc\G3LGbbqOqLhg}[1 )1FlQewG8z+'9LZۜ/輭JJ3Qr?ejbhHvM`JYwUMJnh+s#<)%xL%|&̐Í|{lk׭P/WnzY ?:i14 nPF$-N ۽9l嚳=u nٙ vF*Ŧ> $Eq br@qT].zY-2kaMɻD.چ|f)9 d`ćV?1 Fw%a%4g.b[kdh&?aEzVް{n,Xg:;ڌu;qpGtA$NgxC[% ֎J&9 i UQ$693s%CTYOKӊFZAB3-/#h&9H L4 [Dlfmb ِsN"o]+S2rL[+eZM#j1 :jÊ\igV0rjt̓'(3ݮ} Zz0}DP!0h&ÌS,]"Kdw,T6LO.;;Kpm k0mOC9#NKҸ r6RB 1yyϠDCl^tV}<)`ѥmHIչgD乾u2mN_հ%&Έ{_ӋxԆA?;GNAcG;F#'s_8att2hg5pTu*~s·{eD%=͌PqQ:m?]=όzo>Pz9%siPZwdmf_upo'TNXBރ]U֖܈&vxKòiudL-^R@ˡbKݔR`[{N F9gBd9= ]^&, )?2wQ)qSg(|ȕEнt\ PL!w*b֚k6qvк,Ux;rEjg辆 ȗCQ:a ξ>=dOx߻wa IF (cr٦G0),qk`i25Rx=E E>t~J\KBIK&3z{*'1q%'Cp9,*6!lCm_zfᦈFx^!1lK*R=yYux7:y6oOzu<6fO" aO\ʼn _= bڇM K N_b.zƟ%Jǚ 8aGrr l&VFwLB8e%Ҧ-ON2`$,8qGG?,HGv&TE]yezuɦC;Js㋺#9;>ZCh)zL}bL YMSԞ[BwRc^Dt2}"/fSK Qt,mjl窥tMͰ8+3.jC_ouM8i+Y}BI_kANP)Aq%Z1vߎԃDy[Ѷ/ps$Wl$t8[KF%=n-~w !nYN#p"}  !7XVpa qTDRVQ Ix;%t]z/+akxf Vq̚H0P6|oJz4yydvJBySD})'`$Nwă0 4b /:"^,gK_Ŭ RGtP0,G 3~3{ ɹ/kPۨSl`$y1Yel}.>bdm vs)Njs#eMŷ|ׄ%k5tX*-SS&8+ՕiSԊD?r<9pvNwbgt۸j~ugJxn:luYKãXfD`Y]D1/Ut %z^d?B&s*7m:l L\0;T$ƩQL)D8uё7FHP^G+aOINح2^Oً/(Rx6-~#)$ji~GlziMjL–m4ZUׄ^R&9G E:*f+#U=EfL+x_8#'Ki,|ދa(~!_,׾vUsNڳSZܛdߎ,.mQ$4~Oѭi=]BڪY hFCM{m8OQ $(}w؋ֲK(;M|G!鯷# x]l"L-v4?3N/kƇ'Γ>tOviHFz$J*m_(U⽨hrZ+ -OqD 6MgF|R }@WrT~<, $Lqdɥ( egS@֣s$ǯfkh.13GպWJKKwrxn6:ODG>ǣZW]F4"Ov~6+a Wݍ;`/W{Qlb)!٩mjc!jjFMNxc]}OPדGK xTv)aXWI$5f,bV*޷L r扻g4BxG=9NK5'w.W:zf {^̔]JۊSIwDfde܊2tN}a)'6ra;*k#"\\fôN)U"BiLd:L4Me0CJtЯSZKrJr嚓gٜqyw47=xǛjzn̸%A4VOw"5Bk9;s*(ӟ|`^n=~ѡ$S :݄(x(+/Ny{vJv Jcgb^ȗw:m!v\E:5ALةx|:[tvWMX١aL?gf@dԒ""8GV{_?_MiMdz!!Z i::>8cU)GڋYb4*D,DBg`-[pj^Ϭ:.FPvJ?\Zb,T 1^H 4it_Ϩ5l{p㓐QCw41כ}l4b07!<σhf?Ѻ3mV!Ѻᚬ}?4"::dFQN lcd5h6QH{Zvq%yҏ2i%KVxfYa%K]mt4h [.k BC}#pZb!?w+YZϙXpbӜ+,rT|[q""Ϲ(6e[c桍w2# lᷣ͝)YcQxW(`.QL%oq+^ք?}Zik ~rO:O쓭8+HSYs *B֗%y;8:}c+&CZgĦ;a#L3`^fFOtQ\r|${Lh U֞ ڰUb6HLlag*!*ү#)rLӳsh SɁIkXl#`/IӾ *LʲԠvXf O'C(1ѥ( yf[cHRb*mre褟n-4A %½Vܾ)@`H#5]LZ{aO +Dd+HqM*;]m J14)%QO(Ӗ. 8hpi2@t))F_[#r" _.Aԋ #?#bql[)%%h'ȩCGo%d>#wԼ) ѻP{Ǿ[t?*oųPu_4qAbSTiइŲ[&mHF>v2i4Zز(\sz2"k LB /wy=;^-oʉ519'Vޅ)2g+ZS4l Fan[|:'>CqOMwLl\cf7A^8I,11k8Y{U4DOtiEڎ&G5YG(BvU.+ݑ+ 3u`( zzG^p]hvى stlv.9@QQoTL9=DV!MD $Qc9eE1'0XN#wgꩩ,x1Q!` ;w)f^iHb2܈9#NQE5:[<. IDAT>8ń٪NjAGmtY%>{%'Ŋ#L?ʘ!Zq}}miUξ<R+Һ^Rө R#*rL1.ٸ;2A-g::u&{D`Swq$1vve;6lqiϚY6:w_W+kGoecu5VNC1>܆:'>GQF.tf6mxTi)T͟m$ Di&a؎ƭD)DGFCݐ.xT+U{2 ~G^$iLdYBAq}_D'jf-6S U^&BTk3\Vvq*n{/5qvŚG,,2m>IncLsOm'𛦸j(Gsuq|f_]s~mb{ft2nǔM\lݜVqVv[s>3u).9:|$ް$U% NS6v)(Nңqi*<6p ߐZ]I$*#ؼGubb?Ygn,o#]̨Riڿ_\t/O%aUhu]1t'?& cvhGsa/Kç/'/e(1("jmD3u T>iz'(><=M0]xmz:j>_v _YEul]1G;s';xMnܧ;TpYHáLg[fE|>5QӢ%Bڰm@X7Tg^NH/=/0z0yX(#+S)S$@ǝ!I9t!ߞ [֡3,6}R  Pԋ(}LD  +DŰCцsOx~Y@k0ѧip]/$ edGȫT(5m{dُGŗK 4=uO#u GX;x 79LQ;\B+ǀ_,Ȍ kY&nE#n6 @/1b~'r$Sh5Q IKJiyI#ړNn=Qik;y}r٤&R)14I/Sh仕 /aXP9MqXMhD4C|u/-VdLffxfX >85jq6s4ښǚ`&"|ŝ+64e>›7qƨqyG ѵ|cՔԁm(W2ƱQOi) ՐVDj{Ug$Ʃv4jaaNt4F 6! "Q|҅/_6X|^@&~2%g1e">wPK8az[m+Ӥ wCj":4'"Wlb@J։VC@w){uqhAWW< x|*=)1 1-&8~l]L7*Q}kzՔ;u"$b`*m{pYCt8?#[7+,]|)U%$뒭g 51V`M/5Ri4M+-p=z>%1C@iR ռnI$55'ݰ5us&غ Kk7L 'S׏å GsY[svǣb!E7 :; e䭹p;^BN:kCPS{$b3BI[y,S&wFJCL@n&8*D)\%)t#_j:;*tT=j҃ZvNU|ۊo{cLO(S6+,͕;nG #/[#KO<09HVY/oEDDbꀓGl[\H #+K{P?_<9B Hg.O0EuHX 4 BPUNRvTǭK98pU|זa]miZJۘ(e?:}2S\ &"݈`o@+59ŧX!Kh&!cظu34BB t?QόCxfp#/ep\kV*φ[6x4O lq`sBi1*.(xZ7@'SSj΃T Lٔ2uvsʄQ:s' D5GA9FO|0yC4?$"eQ߾ujEjiL^Q>c1{J9 Qv4;\qr9u_ˏ_. N߶ C֬%f| ]z3$S!,ϕ܌eB=8HS^]zF!6#\vˏ=U&{G4}݌&Pt؅A$ޫ$LօTQ<<}~ .thmX`n9)D AEr>Z~}\| r:&')2OTdu59e5t_*}0WKdxBobEnu(r muȦcFOmfL%L/Ol/mm99eSU`cR:=SSN)HD0i bya5kżf.C3VV.:Q03P5$P̮bXA!oh͛bėKrꃝI&^Sr(*WI.ij [ +4O9{m^քs 违ժydL(ZCQSYh+_砱?PXq%.?6=JO׌X!XliOq٨IIuH$WJ$ٚ&Rc+(\=A^Xޘe<-'zw|4J|rU m?\rtA^=rM']/`y&}#>G9o  FolU<S^VwѽX_ɤ"M:4z}WrMIo]fCI:q6ar>Ĥz~~Yb4 h9YOB>q+wOp7֊h!݃7 /_Ӹ ln{w$cAvϙ"tԥSE/z1[ M]]2ibLta%˩N.G0:]0"l#bؚwZ$va#|OCwRIZO[ENw$j'}PtTO+3%n<.tEE/Nq(lsNׇ{+:A>&o$b.poeT9XCtF6Oǧh6 SQuPHeԨp !Xm+O`LF4 <%\0$̓khe@L(vO9O2./M_D$ڎÙfG8;35$9K On\Ce\o}6lE ;0k(#tiƬypţsLȡ7nvbE)Ɔa@,v~0r(cy1,92>+upuٻ&hiVN [Kz;WK>K( f>6اuB^q^̜MW81˔ Op'9šSsxԎؔk5rs—bϣ2Vo4Q`R8{RV}O،Y\^͓o%0*Dۊ[[// 3[Lܻ$i⼬4QW`Uzt@:R },S3/x`%I~9zgnȗ%W[$Ao[Ʌh#j4 7D"0ԘPy >] sz Dg܃WnΏ.Wv*ǘ5Aĵ9}Ss?4 e\[H/Z+)S#W)c/6)ZɒIYj; H7Eq̨ur:hgԪ]1MۧM7Bb;`/~yYTKP4o@QCṎb-D4h|a]VP:D;ȀHQ,|R2ٍc) ΞS^k]PfCԘY5K{㚰?EkB+Kh/[5i;S9;g at^Kq%hBsr5}~440>)) ʳެ 1x"sTchm `D6Eosb5#v_:P;N$+'#oAwjlGy¡:#=/{ ^w24u 1B9>4U[;imv9v ٵfg$^btYbt?qi/iag MT"+Lk4y!dch>A^4/(-y 9e@ͷSy ^M0/g;/:7k۩\')TNhnEPXlh tQf2]s a|B=:Fpl6As zOfƚ2|'INᎶhW6̠a{a5a_"œE\(GU·S$^1<\BPBڛ)Ao{*7Fh!֋b'GMx-M43SuTbς=i8!Vl (@[+$m*t 5 ,݆HrtNx1slSMwb ZZk1ػY'۾&]o+fɦ%zYHP݅Ŧ)KW9* X1Q|Of˚Z)EVˉ5I4b:PK#H536AKpY]e$7NFZy$B? 0֚r٪~p`NmJ6ôz6\mL&AJNn'SJY@Fpmi汆ԅeM,*eڀgQz/f:/ߋ\G9{wVۜrNmmfjÏ1JV^(}ZBj{G$Swf՛5' 4w+VqY0j.jgoG)%oK- ^3aqʑ.9p>p{S~</Z7Pmc.^Q# p]U.X~P\ e .6Aŧl[9Zyl6*[ށeZ;1ŅR{YMwh3Ɨ\ 5pDm%hruXeIbBL_\DC;g-,muxV5.LNU2 XX ) Oq4:R'Zb ֪ :v ;/`b:9?mhA w&ڑ i:DV~li(%vg| D =!^zO%aXۈfOHAqAk L:ajESPS@٪[5RmCD<+QI B$˦a= d:Pgb,ƀFDlCÈS<9hv@ۙv(ia7ۓY#0t/'˼?? {7hmX}o0:#V t fiB~4ހv ӽGl@ZoF;j9M~ͦZ%q gklizjt "(X2PŪT䀽 ]M6 i>qң25I0~aja]`Kф7ꏎp!N29/늣6z 8wp)6C22=:u?ְU;W/%HMpY?tgzz:ۧ $aJ=o'_. d"EC/kv~Aǣw16sDt6ntgWAy;MfÝXB,똶-$h4X)eMWgǎ1ӚU\0g:IֽNDsX{/k>ʹVݺ&vq *G~#+O׀#?qED(.-{%_gL.N5:俼aA4DaPjN/bv=6% ZjWɚF< \?yiD !{]ymsb^a~84H=6 Z7X 椉1 (ީagܥO~͜m8K&8N_|8&o4c )MVH,:]i#]$}tgo 0jȌ?X1~!a^C=A1tqsR̆[!nE`*o;LCtYwǕHvJtW6:;5r$>>J^K֜y^y67sZqF2ĵ6+S. :Rjwb"Orq.l:2iL{lL%r^l٦DFvv51c'9Q$/8ΰyVnt}ߪO{UTqS\cL"ޟqQmsB O+]i2[oGţ> 8{1E>xyaݎqd#: }} Gk#oL$.Tŗ(ىEdB(FOmW.@qiE+ǤLp) E d0KHw-.KC▖pe*<:?k%I%>#2zE2ob*3#\UUtwUV}|i$(flD VWN4(o IDATJk^XYrԺ6!b}j~I>埆?bB6"~X"djS:&cݺ9 xbZ(}V3WZZUl}|ՠK:F_46~9Q4S=@Γk( ֛3|T]'mG"bAK6abGMl?{&SYedio:`Gn?&{۝%&,ɏjS[ qSShxk¾CP(w%zT;ҽIR|G3㬾٩N|.Pyr$8V d&I@{A̿ZӠiuugִI{u%ۃy}uĽ_S62ͅq_'g/++uo<BZ xi1zeh+ZVX+]+YX%P,K/d/=iB*Lgas;˃bXߏ}>\#\3urhIf <sÔ9?bl/{mY d|lK;c8ڔLP{Grn#dzZ*G\ݙo.@xGIl Cqrc:}&u}pL;P' 'Ê B^V"V'6etp9M3mώH&^9~!t5 l&%vW  5il:ځ#cIln1ecZ$%{6J%zan n@́)SRG Q6ypk1$@Li.\X)] t]V8>&AjpcEt9RF20 نv/ڪJw8G3F$ħRQiid.YM5aZRbӢŨϻ4&&!l tRyi= ux"kP a Ez8q^Vq -M#z5w_ &~|avx5֦PN/׌jp!E5\LTz"ݣ@IEoFKX9? .ɰRzd G-<BMDWGDxb]m45*BQ%}*T]>#8GO lTЅ*TW<1_OV1,hyTZ!NiOjwR|ťavk}pjT`Ϸ 3Bfދ@)jOMS̙m2:ꔷf+vd M-heqrw.,|\dWcGQ@^l 1r*5) 9%?C_;E6ȗlrST[Y~h?а>~L;6-7�iwM9`LaBmsљzu1 ;ͷ6yyOi.PCMH) v7ΛKX&YL֟#.Ol}/O+v:݁MO};G-Yn&ܞbb 1.AX( ?m7~Pt^2lu\Xk"\x.4f*!SǠ錜36= c4DCXBtV iVԜJ5^7,Ķx9m}?ȕYF8zP@0Q΄?ѩ%wA nRJm}'-(`\4*\mmz#öqZ q0mmYA(t5y!;ϣnEM#kzQMt>(ȁF~KH;7hہ{i)쒹Z\sr/4-y 3,Z|ODN8nݽY踡ȕMwwnj~$jD?s/e5`jQGr1h"G_:p=f4WuMGi){fu/8k/BFçf9$4AAPpD Xse=xu8$G.hi%c:CK: HHR5Gh +>]\2*'2ؑdZ":M/KaD'RPguC>2inOep@ב+攬q>/ ⃚), H>83=_E18m񭳘FuaJeDּ+iWhT^c WnJw_yJvN:*7 /*J}mYY RnDs ՛x}8^' "}+ ķr a/DG³V+Ljx@R`^Ql}!=Q;.=)c x":c8n]槯DYGQA ;]u:M (czV% 5`(C K:8&>ZeegB3ۣ#ƜW;uj`I%8ܳv}|rVƏδ,/"P'h9(1F+KQ\Aל|N ^GNFY;7hxxNoiy:Vk`?N,4KzL(.S|d9luZp ɝ]u[wM%òtfٜ{[OM;Fx==avI LNw1WQ·Q:\VMji#Bh.6f}%dIٚez/T :0%uN@ϱ3K؜%\ !ËL_6'V((]l`sT%!a`H`e G͵up~!Bz:Ez}(g'J%"^6[*aZ4O܂Uӻgn5z 9L;戃T)NݐX[s`lfH'|ߋؠ[,)[PZETu(,Uaguv*>i2{f.q!qf K_V{x%Ά-w#U-q6ߣ;˪(mM9¢x1ts֑**doP|YƊeu mψ$aj,-|h MO̶j:Vr.(%d 'Q:m{=[61knz&Vćܹ2TQ1fKٙ.sI\ U#˂ U<ǿ?D^ iS8)) 4l8 x\0ti-@59 @T!!,*v1[b\N5NjLnxK+\cȎEN6sl^քLk){k2v"Q*gǒ:6Ib?=S'a5}]:wmޞ?]"=LVMà jj3=U82x#^Wu5'jw,($:%8n q6m*, EѽnJ!oFgѰf;o5ɍިд GOׄ5}(Mҿl +͗t_ .4"3Z哄i}ZK+ Kcy*1UNhkǜt:׻99`fV#v (?? =6uk-!>#{r&%Ϩw N:g  ʾҜ#>rIQҚ5=!A~r*L9<$uwiпWH]](6F^{|#qn_R>έ1V#b(ׂȎ8|n^*{2%{kPHYثĢrb >kïOmd̐CqCہ_^\W{}J{rI<ۭL;ƀcRZC&cRtnt(H1aZZ9ŧo@] d9 };:avqb x]"%8HRJ|ʰu=g![kJc RdQ}g,Ś|*xrM9-SD;^6,FMZQS)NǑG4mb(X1$[NbOq)(}!YXDb˨(}t) 8} l`W#37"d<#-aȄG#&d1R<'ɞcd;7~Q?~usrੰYFЭ$IiC< Z83T0̜>JÍ6RgBZש6 wAg6K`/I|&V䚌yq%X!2Q׈cEϯTRjxIqy €P.MՄLt9[8OW!$QVi5P8dpIݻyZk)~Ԇ1OM ]u]ot6k ,mڀuI- 10%#۷GumD\͑fPge@u!۽|cɉÊćp+óZ;r|KC0낔XFߎf[өh=Z, 9Zq[QAZ^XsF>x \5dؚR kԃEn8VUv!?]D_j|Fz%Dpa= D}xç-mon&X85%6KtGwJt0Yw,>&I-`gViP@.Lw+ [5%=C@ZJRVN:~yOK$=BiokPH—EP5^H.14mw~QV4J#t[ > ^$X(mK &Y\Rc Yp`H/-\H !-FDŽ-OMɕӛkYN[k\(Z$Toe=~޿ɤKH4e zPQ/}w.4tBH, ARR6N]NTGw IDATȴbM`7!&$]> NelC(,]B(kykDN/z|k~Ha]m?, %㤅?ʒRn9* {MHTTD&D7m=k)01`5wG|묩DHh.7 h=,ɦ2~Hp*xR˒ⰚkNXzZbna}|_jrӿ^y]!ew-M09ގ~v\2HuFHܦ(5Gf\j(qfzO+}; KDUdsr"AmIz!qyl9,^5[8mn:|1!5 M?E.8X^5ZV;DJids%*'݊+6YϚt_'s8Tٝkb)f\YYł׏^.ym^9%C9o{qL ui,*k-B/M۲(q9'21\5eng@9#GqSӒ򚣭xqy/vڷ#C>, }{k!mL}*p%l0 2?s1NjRDYyzSIIYu4\f"ݯc*~aai:hr=JڴsxD$|CIŁ,ZiM0LR,кA] tmNX"^.mlTP7 G1纯Qm-@Dyi-*8<\h: 8Ņ[HnMkJFΈKN F5Q?k5_',S&LKC`>u ;ƒ/[t/,ئtAyHL݌mY>%1`EɺIBꚩ5)YtV`a,-٬2*dzzMҨUY[/{:Av`C.fE st~xp۽i MRzL'\Ƀ)cls'gt˚ԻXrQozyJ(50#:[ͱCh@jN$#;ұӝI Ϗ͊Oo) B}#{>s'I@•}X}eMzwNzCo!9̣U\Inϙ\.ՊbNG5ρ~#.k 4XĞ: m%fm7j(W,&Q)r֌_茜1eZU$M6!_)9*ϼZ B@d/VXFB:ZM=kI?:(e?.g()=:e]lS62~WQPu1UHOf#AUb%~ NaQd:۽JqޟW^ijim 3BVa>^H*uDBMrX'b"9Ka$xG+[o/kH0ok&ž,>x(w $G>3u1ya=:SwNځĵӠw'jޟҒkJ9(kxmŨ=y{\ׄ+Wk3WzJ/xH44t.]%8p7IS)1,L5{rYvnV5plNDT4|6h3*,WN棸Hg~0x&K>}:u0h7 8O:x5dc568^rlgw 2iݨ)%Yԣ4Ԑ:e'.2= wzj !gͻsTVx}`PC2+,8bt_ZS!+_k;b; a+hHpu k\XP`$us.ʨ&[3U[MdEޏA\'߷W+Pyqֵͭڟ0οQ\mXqBE_4Aq Bt&{$['Gr IuX/lύkKvMjK%gIW "m }NJӒ-a,;Pi;pvPڗDIg-ny! &S(w0tIdŊsPsK (?w ~D%?[a.o=|w&`tw( |b[Mkb ndZ˜ԅ<ݡ6\FF.V_i> >m]۶ i̳8Gq> UĪLB;Q`V1M/ԁzC4Y.wZiIۦiu<"J]i&#\"er)+A1֖. ڶ#L5ֿaFm@Nq_+~Aw=C/n7])yƳS-13݆pM#cSQ&N|=\ۉ[Iao+C7NW]*ɷӖݱ&L!z5`vXT-N>ӽqs@֬GslY5 r#'錉#x'ȶ4xG/Qv S|Sgd]M׌R;Y]Bvߦ Eœ-k-Ά5ȗS;;8EU^PJ D׆-q3stT*\F6ЄIPx*uvs:3 Q~@2S ~MkyGvܨI2 =MXbu:H+q `?~_-. (ZR@?-Qf˧-|ꏳq}`V᫵,иr"vb k n(}=^"/t|,on ݋?G:McgrĴ*Q~O=gDȌZ",Xu= Wl`9[Vk Ѻۊd$ڴ7(<dx]ljz=Z}iXDs:Vnddд>&RKZP/8dc4GXcyW_RYf3HD߶&dBkåh L-w`Q(L(gK2YiUt累I҅EZΫXx!sH-s%Tʮ,:mS࿻NSu&XڽSbe,ڱK$@hZ-*'va/{` Y.\_#g㬾KW O7hZ՝~?+EGuzG wml@ŷbVgJVʥ Ej#'p6'bM]\Iv03{.ԚK6j]^Qc "1<#otaC[rz%E\䓎K)nQZF[8 6KK'Q!~_V"@m]Q$=c@"g#nƢH:y "_?!nXt]Vj 秵/|J*R[JJq݅39J< HN8 ՘vejvl / gۣl }߫369G9ή̆^#$/|v{st^7n.kH<={1w&|[p6ö<Ά,e9ewwχ1;>Ӊڬr\N__xn l:D^*Yy_.]':JUNjSGu2AO Tn-X ֘ҹn9uF\Y5gJ6gpSv7fC?>j>Ơvc 7i\&QM{ҜxMf?b3r]N>4pau(@ӡy?'̏[𘤱Z>OްBY5'`j?0!eoȄl%?Y}];Q`GL@\q/Behs!Rz^L%+m`?ktc—kb+u wGu1=Q2&NQSE6~'k$1}[5q.kQ8+.=!/Z=+Å٘w.VއWG J$'FP+(u5{X]zkfyVؕjI`kD0d} xY3o{5[QiۚMH 7H^"(Qz2s5QZX) `ķ oAg0K;(ƥ L)[[.r?JPCD'=[֠E.(Tk>qfXWrj4MQi͒KĔ$NgLhXBv$JE; c.-lc`_^ƀNM^qRi3Nٴ>tA/)9SHMKu}"^rNx QԘ| G(V\jOa֌/4[%DdA& p'Q)_7( 5Dbxj&M6 -^CHS:lM*w.24^tǤ\Gv/Te8Nŏ>8]ک&kȊ04 xr.5 |H61~²Gj;߇)bF=7۔'X\z{ gh٦%{]mlj%Fl.̞Fbx'mr5#j{5@+[Aκ`cuM?H3"rapL]%d?,(e텝}a{'p aPMzV+15ݙ5gk7*%Ad=?Sg4hnYtj(劺K)̟W0쇧Jk*~xnM5e9zW{TM?]9-uJa `G/<˒qt;JpGaԐ Cb;-.Ζ&[[h,Kj#48k=v[m*s BYQ.)rɻ ufjYl. v%8 ,Z;ڇs%Ce5ogQSqgLR9/Ggoăsa8!R[3xjK2$ 9uiǚGXV*!mbޏw-1*Jx$Ӆ4aV8:d2$Dkbff떽fM[3#=NknkD-Q@Һ[v)+\@qH$Juaq(e1> )-& ./|# > ީη9:26tN04Ă!Hh9vGA>D8Rf)";>r:s;1|,_2GAtABUXw6bL.PgS@e9|/z2g,)43lLX5B fVVqm [x N)0saJhNZZńi||z2)Sٻ546X[sôT䶴Ѹ spx\8涞dwbrcP H+\~"Q$9+z@Q{L!Cz7NS|ҥPoϙ4g5 b&C>f k+'Rm75CS]%Ddt/0Em_.9m\uNaNl&E?\@ȷd \SėkrM"U7X#X)`sӅGSf$K[þ?{1sqmUTXCih8EWgE60jUWSsEYj`dYfD9j3Ʉld_E*x/mP6ן:tpeR5<6mf_2.G c)FClSxKQvE/N~ cFK(/ݳRw]Ǡ(TN IDATmt~֚ E0 9;n >FԜX ݧOw^XbbIqTcU6 nݱ;;eaP6ƺ)$ZnRiwO4%CYߦ4c ~g"]#ͦ Έ֎~#:w!*J4Kٕkym o;׿u&-PV8~'a^+/p2Bsۆ^ڊ%qɉLMA[*Ϭo>I\& _D7Wkk\^ [Wed ; ]#td?m JHͰ9Fwp~6|sOk\H|B*ɁxN bǕ cR;SRuwJS":<j{H3-U7ʵ6 96۳;?h @wADS`c8Xl| Xa1\鬴sT1aӰ5Y0^mLF~Eź^ړIQͰҜvgB@6"pQ*Z2k3^F^,f$NƑ"L?IT`A3K#sAEjKYG!MVrႯp65-\j*0XcNIr7dôfam obw͝.ͭBƟ&bɢU-д$HZeY9,⒁#M=FoxGx2~hS7DCNik9U*{ݒ49ބY1yձ( 5ɤr%#u3lbƅ;jf{(@E@(v 2 ŋOl\>HV6ar!U(,w,i򤋩y&8W{/:3:ǡ'ak-"7` g7QZ''.u4.a8zUx\SO?BZynlrDn:x5SDXƊ6H)`'Wk0QS\Se'~viĹtfI8ds_?N7㯾i[k x }%qx?S'I568NڎZ;Zj?Ē:zj4 aO[m5Y 0PzW!ŴKWapM_cxYMvY{m0r\ sϒ:rLOJ[۔ɗGϡ{ׇt=j>Z&Lk5 WK;4ؖ)ѱpP*Z8٘m}j[4Zu?EBȽ:'mb{{Yu<_Wg$VMji+fJ.#Hlz,f?Z`η*Fy/1w40|IrįuքlQpkYV]cY ZU>X6_9\\9*VK&mԺc>Mm, } .2ݴ"{Vި5aP~$$>+i-KR+sNgjҤWljM쑌C 9~t&n͚;C8~4v/Ͽ3餟:t`aS%K͵PXynKڑ2,1 4, Vq(abU}*zzwv9&RЍ`hj/]9Nwo `Bt=7jȰˀcbvE'uqE`j%#s,֒.=LV6 dOtO-&lFv?Lr(y)k%O{ r".Wdut53[{?}BFY~rk\_rsX=1Fj8lEPfu+sbcDK?_2R/w?$V:aq1/[緑1ZvA;^B?áݽ4.s:Gyf=QZ#Gmӗv4%ˁ}]]ַsZ|Xlqw&@{( V>eUfքH Tw}bb.vhR8bH.n[S4 &/τ THGb Hg M^ߏڧ6y>V9D-QŴht%\jhM3_a`zLJ26l4TseI?ߨ+ӳt*XPڀLVcrGA,*HhFk~`yWw}[PT+d:MYyêcm8Dow*Fgt[o2րQfB?]};l M5ȋ~?E RL*`ȦejxFxYe|3Ӗ`в`%F> jv{FJu2Un/AT5s`_VO[Y&EEFNR"I챏)w;P%LӅ Xeq+QƈWKӟw?͢&pe'Ŝ9ŧpMRSt:gSň~QYZ +k?YLCoL=7`6+Ɣ=wߘPZj]„ Qس6WA|@Ӗ}|i}6חC+I윈`GS~n)<+s,霉DQ^CD>JPNMu):I\ #YJO{qomRO[I۟nϪ OJ 5N<֒c."S\^t9e2oLL7$PssveMl d#F.<~XWMpNcO[v@Cub22`_M\ Ó677֚6PlҐiddzqwcjkb>oD2=M6ȁiBښA&(:9vbOak(5'23npFKds9҇]SROZYgvt ,(w`Bsj0R(7WDZ;&GCEdO0⺲|EKB ɀfqw-q '6MR |?ȘLEM#/O(!hA6uw![TQ[tr8/ZmĊ%'cDw'`3#M(Hd/.y>Zfӭ9lcc#7\).|sHܿak&$_ei]xoscR*Tj6܁1inxU3ar}^׆&FIUpJA,chmu/] V|t:b%4ڂL]kP,peC>4O`#/3񄛱i&`9 3!/HC]?+Q0CT1d/+Ø3'AƒlZzOw9H:{d{DkYaIGQ|R5':5VSkE !v.Qw [#i⣠E8?wi25a!0$v⇄';/wP Xz8jӨ[%e-Ap)ǷW Umd|l49"kO7>"IkVϾ+uqk?vр@#D5r.=K2N1A S5d(OIDȁ.@î9K.O[ŸoMA]GdY7xə9{i5PW(^+B9[T.?C=si%zws,Vt= Gr`:rxyJߍaiO[eՌs/|ɷMB*t)FnvX= FԂkayUknMڻ4Ydyj̪2B"%$VXUq U4z3#u7;Of2ZzrVxl#CNfJF~dC6p1GmZkDIT9ۻ#!.׭ZsXMe3\?D8~A\&!*˜62L*m3Ϣq)MG9*3@SUmjo&EUkӚ4y3 m`wN+yo}l*2e^^j"'8d1>6Z߹N έow>q[YA2U粒mzή۸dVa{9$WTe|Xx逽M|cwi}.y**u+Ό}ݐ0tҚ?)j}k)~KZ S9ym~aY!r_%gbsy}r6_/#0 9BiSxRzLM^(r$a)dDvI\sm[mӇ[41:{k, .`;.@u"}%n#őf?gvZ\0 1~ߪsfQD16vh1{PGwzЩKS;52C\;5:{hfyCr76Րֹ}qdHb|<;`Ke-ֽPфT+%{/.E"5𬅰uMt詐.tE^öR~oOuUsf[m(\+eIɉsj&&BA!r-SL煝 S1IϙY'>YrFD [jub?' (,CTq;- x{4F9cP)gp:> Xa3"N0Y۴t;/V=~窵NٜCtfZ/l1EDӰ&,1ᲚݜvG0"Ix^ ^cHbzcTi#$L;Xh6y)}bAFH8E{(,z-[5?8g7ԧ!TLvh nƌ7X=89W,SNsfԉ\*QIHܽtx >yh\޹ TJx9qA|;:׉N^c/'L9!qPH~cz;sA+RXV dž҂FZAdu'\0Y0:t|.ÑX)*\LO>3ÊD1:ɼ 82&@x y-~ IDAT6yab;7Ąr@:- ط۶#OdA\]gw47 m/X.V<7*VCo GcI`ޛ ~n>d#&@![083wsךA_ b=a+UӢ؍d/x2^I+6uq%L3'XlqщỵO1L Ƚu|uf^_j";Gǣ᧓_Jݱ˺2kU]گ@1Q]f:sJrHw߫mFB =K;XҎӆU4g({X6;戟ّWGBu">OMxT3;KjqNxrX= O7V,ƍ<*v610*[ypףwuvQvKGwo 4P'4V*۔E:M0˝C? cK׊LDk Sx([/?hu^x)u%Ƨd:Bԑjt{qV;}\M7Ni;64mFwW` W`kFBQA"5h7OnV3[YPh!{%ZM)$[Ş7KrGvسw>E?g,X 1æq {eɡ?BF+ٲU\ք^`S:2+jz6z6k&Tu;| SR2IJ"vs}wcs>`2s""ȯU Z:r$w&'Ljdpmo6BՀ|0HTZ#'JBPy9;tZ| l-3ߤkz7s5c_kx\3M{1=gNR6JŦY(zJ2Ez8$lL%0j;D ~|f2]b{7 c >:+L(Yޞ\.̴G a\Ϭlk6J絎br0@fᣭ =qK[iM Ejk#RnQ{m32|$4ww~t9/[RtqBU0om+5fd^ȟ.y$ @üFv G+:Jhh4^8hΞs4{1WX\e7V. ىڐwMR{ïZ^jcZ)I v&n>/ GNF~ף{%,.}E! EE@bŀ\v4ϨTӲ&pEsbC+\e阍1D\֚]zjl8j69YWhrZ#و=⓺b5 窫zn[>pD%0U"c@ڹ{vjyaܬ) r!>k%N8c߽mPG.Wn;1Zо=JGա2ra//ܰ.唓7h*l'N٦4Q{:yf2"3bXK*{tYt&RK73݊5f(8 牬/0(?i#zEp3qAHKI@):Q/Ln탳`39%l"ŀz<ბeʚ_Xy#.ȇo6 TNTH{';r!HLNN.lAuZ/KX՝i4i̶?,̾`Y {߈閧:na՟CN+[QQo{ gUoݏV$Zdk-{yn jA3vŔ>3 - [$/xGbcڱ\)ߵ$$)D+UskV9>F]@𕶹$(D&7RCV[pv_oߝS _ΈL*]` KVsLt?Я[c~eeHE] ,:EO |Vb;݌.5g l/3d0:.ϧri5}6zBW)zQ }ś]o ȂΚ_M!2]H;"Eڥvĩ[9W\dzkS#( 9RnD:ynkY"i ˆEכֿsk'z*ڱK+sUo3ws{5An3 @߀BDj l*?;yT^Wq@SFTNޘjpJ75RQG#Rc|E]o)=BBAEDXĤ QdQVFoo|G595[;Cr5`k}?{{id&zvJv: "H,hMkL8 LzLpXbpMk<jy(y&}סzVowO%We.qL-ہ/nj gډ0_pX*U)'%{HJ80Ť&&81e'Z=ٺ=42OQd$BH6;{hFcKpl*Wch|ā&yw24GNoO5_^V@HihkFr cJ憡GtLYci7(QJ;]nj@d 'HOG|U&cUH3aj,ϊ{43D,S_" ?G81)L2Vjƃc0u"Hϰ;UfEޣW>'2\=ʐ,t6=^<[W{@19LreCoWZmb:OY k&܇Xz6$_S5ǴO)@ڀ,|cŔZֽorڪzf!V~rʾPjL G}fN/'+_=|R#;b  pľҟQ'^Uw=F&:MZ4 ̕0ew=i2pb8v}ǟ7\7KxXҀKgj% fb@>j`.VP YfY;U:`)*V+m"twnjĕǞ{1&Dg2E=fڿ!|.|S{sOMv>7v+f'aA〕qv,X1-b҈I A&de7|w&I >Cot`¤k$Tv PY.Rw61Ga0mV`iSu|rgP{dzǷ{qndQUm[ #@SQlZs v&>Sr߫Μ/{:LB[In?rS?\O:!׈?w XCDT9^YD 7AMzLgƈ Nc[4Q6Zs-~>OX!IV`GXZO9"xz-iOX(řK6>Ύ$ŁktaԨM9+8UZ#A`0d2 $U l/GeV2(NN.sԈCE5{]k5 /j(}z(sfHU7LÞ5y@Q9-*= >+a>iLsy|xTUbR \N('"v=*2|:FQXq|_T9nFllbbh1w&3j:cJ眂b?oofuٳHZ(=F#ObJǧcdNvB˔n.| ջe1sA׬$4oI޸F.VDbP~+hGRH0GdJ?|4U MR,`ڝW j1=U,._\4G8,v6`㒐cf֤iC`+ .ՠ6; >;U/|B3""ykNs1C;~>غ5=/&J9䁆(ya/K3Wm btY/o&+;Icks MQ`(ylק*iBSd ):6Rb+%)bUӜ89-?\͝^V{C&M&~Vl+Em(m/rIоC8eȝkL>Qa2؊u0)``W- q:)Oi#cc!`w,1E9F\wHvFnFr;nDXىﭣjm+#3P<;)``ȊJ]^:B|>$Dž)/7h lӈڙֻ1Hp[rW^_Q~>ӚEiKG]wؔCjo ͦF: 79O,Ok` 4pa 1l@i-j55 Gt4Ƿд`3 Op+룢~S<24u=xmk[˓_(%FzVsP4;ٓgk*ih96ƀ ;ZPa˳"9H[`@'Nb˂߮g8.sM;%܌.b5NHX;ro 06'{9?KFDuV]57Bs Y'x(d_h=y:oa|xa*nXZZޝaM|EFցځsLnGd1>  9iX;5>0wӹ y`/VA'̇~BM1{9LJ'~N+#bB$"Eيa+:mPkS%W^ 1e/T]Q=sfѡgI #zG=omDjNs8Ε*`O5Mu4)+9@SZ[@ &F,.uYY,1ar0]Ow:x!Pp"j ˧W"3Z?Hi8V_yJŕX[S ؑZxZzjtw& +]ߥM̍&aG@j'be8ݙ(U)0GYıS64nx&E6x!4|z=Zx*DE_sG2=)rMꙗRGS)}w#׎z~Zj!H=f.Y,ޱF7ar Tz-\cG25;}l [i%DYGmvbXa^tu:V!() ONҬPO[Dy1mmjsNH @+/"3^?]7镤0"H~΍{ܪnJy'#Am&TE8WbgQnZ7awlb(`64oBǎ߮;!Ҿ`֯NĘ[a`u/w_R{%lV|ܲqAw n{5{!L| t](:,>rלc@Σ') 'Ϻl޷j|`U8hѹ\sm785MYfqLiBkUהMm=GB^d/@ea5-1c49C>߮{Zm2ٞVХĠ St|9e/G bO[3"Z?Χ૧&WUn[c(^b]L#)hԚ9mmrRtAtʴS]r.gS\,77XqNMZH9 uqZMkZ&w4 o:\܊lz!^WU$/NZ*1!QxYPN nTw:py>mc]""+A^4J9"L!ΎJ6 nƴrXa9 Gޯs}NSkN'V9v|%O4Қ=yn"6-#4agUk&^[{Z>BK펊;۵nC8ڒbe a^8e沢.G/):YF5>jn@ șf0C= &1D[T쉎5c(lW0ʢR\nB2NT#$^Q$=1O}]f :sL&m{ͅ8<;'saRE:^8\9 9;K=Ò8W,:3vE\4&5%E_;j+\L?>yqX,䖭l;),aPHq\wߟ׈_?vYtȼX#\;/_ 7mw4 1OBqp Ow"4$IJ[ഹΙ%4hu^'vkw'3<&/2$S6]] ‡VȲhTSN& Eĺ71n:Ȓ9YbSPo)f\\tƉn[۽7-ѧbwq+e*a]_z0%e +,^}iŊ"ugyG ?Mj罣f>.6 e[TpgQ%vN0/Zz$q" %APuXGIvµJqf1n 0Li6F4\-JfA0eZ;LNaŦmY̵Z"+y#!F|lM ݹIh邏 @8<{/]Ln}:|V 2,̥+^ _oڈZ]0 Sa7:@"uG2x*!:.kBd ]EH!{iȤu\֗c(߁YΩI_=b4כ|9e_q# \m˙IhU*EAy`/&ײ ;iM'RSfR5_#;Ij-B fwJFi`$X4.I fj{1{pHD8&~nkeҘˁw߅rs~Gl dXX¤'u  >/L%|0z6p pwskck{y\d9ʭ?hږhֵ幱`Ra-Qa3#r(#s/x B >'QbQ34}:˩\E?Lͅ"5ڬ%|J W7Z55֖Ԧ֡M1-u)j[RrViJӱYYxHi96ӦC @uwW*Ugp`} rF06Tr+'poǤ`v2㙣W^u|9(R8 #X1CۓF,h ilމ> KSoLyz\lI+vݚ?ʏ;iEuNS󽷑u7uJ2^G/)ńCQS"n;m%(~g m[<dۻff{Ě.`Dy[E4ّͲc/KsvP+E2۔!gRmw͚1ⴹv&RkѦ0lӗ'8`!NHnCw#\u9D՘=K@Ss(@}hIIGGofxΑ(B|L:RєvWap ]nr _ uKҔeSz+:D2]!3ѡYyZ"q'}QH]bn)%Y\dVe+̆f#Gei۽J QVh S ˭Z+M19K-MsG>pc4:PZg^ipN'?h+:1C910VnJ2,;w<9{؍6)}o51N3g ږdPէ.5t:(G>hLji |~jSCDi;m$ukTUֲBއ!B" cs׏ b^;ksdp~ƍԾp.u<#s,!3N?|>Z Shw~^"eMs4Ѧh.P:$+i;,`ERe9*Jio\Ct8/ o!DX}{m.8["4 ,enŊZ) ڽS ,`@ P}V( 3nPc@16fQm*փg;څ^O/K2M̝={`L㝸f3g5i/݆*4= s&p m]4.FV:494`9G"78Y6Ov8 xzIy\"/1: `CھYi&JJCCyN Ȭ{5n)|c^|9%7+\8w}>7@5Hx^H&\ÈON!Lx8jd ]xf V4SI4A&p,\ ӘZ9p:RR1e[O 83 ۃ:;f$y7~1zb/L\ ˶7:BuI>f8  m a- ۀ݊yiTXvBuyMޅYrΌ/\܊Dk}mOZ(&s* c[x*P3i}i:{ɦ*81bn^ -dkiǗM4kl\XdbƚLY(]03AFJ "Tk^lQ5QBŗ|@%7g7KY(-95_M2h22N4 :o+1zVlr|V)z/<#u^]zwa냤MbF+[Hl56^ٷs>_Y3|M|R{"iw':G.⥒7/4+ ROkV(!*؀bF{OI.ERlD9XB1S{XDms0J:WMbv~lVڙf˷[u&j5G[ΆۀRk']*RҦ8$[N6kD3q ;k,4N=s |YpaޫK/1GHu0o.f|sxJM1lL)ZA~XQ_>r8U{z<ŀFrxy%\#(2i$V ŀ\Z:݅%˱w<(#u`eG!q}oAΖ:ŀBiN%\Ua%][.^aw`4y)Aay iĕW I?b4[@xY#ZB#^h;NQ(1!!i |8{,$!KpV>1Qli&,vb`9 Sp Jvg1pJQ"_oeD4;\K}\rL=vBtgӓkpI~”'FWRQ-}MH[ט)Z{\"``^jP+zaly9$|c.Py"fO'Kya_7{/5]H i*;\ xAPο[&O) 'TtoU𶾉ZTFƅUshu1=iRƌ`? %{hm)C]u11,8s܋e؟5gM^4qv:wQe^Д:jfڊִa)·l9NlJg.>+bϻyGܞ=;z:(*PhW̮` n%lcCJ}{S 5T}3EO ""~/H(ZU"~Môw&)Ez+޼LdO= Q\r?%5 kFX_t =E8{OWxOf.cm_ǟ`0:yJ:7! (?s`FƇNSӧf^y8Y#$(>ˏ8SHPS bƟ/OQQII4qmj:?Yi ߁Y :ZJi!% VD"BzLGaFWgĥcOgv+Q֢.D#L¯LxL [8v|fZuu|V !`?յPkCXS6AzamGr?2|>ew79h:68Y2gᙲ<3R$KL:طGuglu(GsK&tR. ‚o,$ݠfڅbinHUBg6PM^<>MԆ65Ul(-/̠I@\.:bGZضMh 1fn BS9Ql͛1di77‹o46AԒHz7Cq4%y^:NWeIG5$rڨeAO`}85Nrz.K0J^(R2C#os&kGipdN3 "/fXҫD?̎rF5$+p^ᓫ~j{rXU)HLZ1ZG[LFp?I8x rJI|{ۭRmmj3fmn>h]stƖ<9a 'msC5|&J x\iWi=kMNDN2`gXjZT̑rS{ﮑ&p\eWueqΆJ$S*ĵ#a+Dp$^,61tyowlZaßt|.R6/ʭtx{4l֘>js6s _B{lWPY$|lq}DUKԁ.̄FDVyl{>2o䷤i B7dZ񴪛פVD 甇C7}{5cC|h5I=9 &/9䟫 ziMF4g}%۬;8tTtAI?~snu9m|5 =1zivgj=D˜)ubݺ0 G(JOgDKu*5xNs,b#zcmN>rEx*R2uoh wta1;;gw5**Q8/X919-ߙܚlQSaw?_8f7InJיzMjeKy6_T",VS{~/fZnvhKT~3xJwsnɀChyOM\*r?BA"|*,ՆؔĄWsק}8GsBoł5:. O:grd\݉iDm@Qlmt_IbX0mGo mNd]7+uӌ4ct4PR[Ɩgeﮝl4=Ӵ28R'Oc68eU)]_=.L5oK*)sf@۴f(:TȮwJWL0"W>b!OdQ`DU[Tl M"!.kV>f$XfMQv~ A){1E\V 46gĺ9%A!wlbz{s#U0~>f_fƶY.Ę8*ka?OX+? Gk/adQ TO{:lηdIǁCS9[KqENDMTԥ7 V9f>>YSeI-tWL/kt=Y~59&?uD ց#/-~D0-.SEk*_.ثMd}ʣ#'v&4s=^ .kD up%q6-N(re"斜Bi5;aej>sդ@V*t/y8YgD!q@1lXJ0ISb 0AN9Y.bw]éF6+l|L cCa 5zGUNkC6͋sF83N$e]kxq++EܐbVv{Żֈ:.c2auMVC_ ֺ[[hl3W2*65pSA 1W2{LWn_b ȑFg\$LȈRjA獭gjUDno4OaS­V'Kٽ,+0ݗbK"%]0PfpbW~\"îS;Vtqe}^O$#~n+ jz49%w|Q=[sbwq} / og voڰD*Fl"x8$Am_|w.DY45 FFnHwZ҆>ln6̢@k ^y3IO`4B&~p:ɭ/kF((`~Iė1 h/neƴSB xT_9z v~ah"2Pup+Q딷{AȇW,|=dJ :&,A G1^aݫ GB&ڔ4aM[s!:_!tc.8Q_ pZih^ */:NJl)yMHߘZqN9ʨE9 sABOg !MVdX$,l<,ປ,ǁ9-p`S{k?ũfTK:S*VEߺ_%b vꈕ1 ;i@J\8XLb5(AZa8՟hc1  8u~cƨw( ^[ʼnRڶ+qH.kn\ n;)3Cbq%ӽon4!5XNm|޵nƝb_w}fkA 2GXBC"i@hC_WU!k?ׁ)v>}pJRn#[ T+ Aᐞ/sNC+!}V"ƨ 1%^ZvJ=e^H`w{LIY^~CȪ0WVxQ6mr"AU}r{@/&G1|:f&':f %Fa[Et 9FHsn $]M۽N≎2TW:y9$vcu\rx+CJ{G>%n E> 7/wLw*.lc=LXNREPIX|3}= rGX^9O}|ڒOYKJl5~%⯯;' ]e3Kb(pWm:gNĶ$,)S鄮\U L$:#8:ͯ~&jFPn[fMȋXdYC_)w[* '/C9X͋k!ʚtM}vD+w4YS[i65g#e*@T%~$ZFCTWGƸ2B;mj>>-rg` ڔl)zA5&H" r5 >NYMSXќ +e2_r@JcV{w-9&a)nI+9F:p@|סަǣ~ԟo%9%s2 q (-1f)N;|f:ף3AHTY0dOa-^w+N*Tdn;^ˆ >_AEQ<.QM@cQ ßn osڰ%sx3rrԋ h ];%$KsDGNmQp]t-|&]YR5ϊw:!iA|0٥GO; ㆠ^kk O!.ɅZ]Xb(t&ѹ.H !Rk ؏ gqU#ΠcMǼ5F R!vaZUj$*ֹc VfkW#Y+2(ʀ@[ff[b_T<0cR^ޟe (iKn>)L4Ǵ4o}SGi} `Yȭwp?&z:&qoNȁq?C&P.Ew?YܧqoVDR;,ţw-zW} `in\ɗ)T݁ekؒMC5LjljmIJAq47ra @J'#ed(Db ET̖ub VNVeca$JS%<]>S5wPMZY@6iIθ: }Xqyg8;.KE|26joz7cJlϜquC+I!,f7 Ԋ4Κj ~ח$|T{pi )%n wfXJl^lD~?a@m>iJ->'lb-O 똜}{/lyĞ M*D1xj;?2F,b|ćJ&g\ S ãN+mU L6lx^p6/G1EYs<)%Fn;Uzwx =2()EPLgoo'!zjB n7w"FcsT}P*zsKO$StV$kW%9AM{iB_8޲M2myxPy5=aD&%Da"Ƽ6qh0|T~3?|ݱe{ϡtQ1@&%b\a:?>G\,9r5I#Ӓ$I/ Gu;99.^:m13,U1H;uis~X䞄5Yeû+99p*{klG%MAcE89&Oӯ?>i{g<_F^5+6A0OP?HM`֝=H<)uh@Ϛ@ UG.w(5ʯՌ.%C:i ?._&|b )u>w2N:wl/JE F}f"TO`^N5Ųʱ1$y\ǚbnmh,1},8Be&Hi"vkS/ R{0 Q03D6(6bs*>=׊Aa`ypמcb ISɌ nHr x7<*L'z}rrGI ?gaO[ J+i6qsq6T7=kDRFܚmusapH-K[5E0&8j%&/{Y΄#-ԯ1.Yqjў5 QVgBq66H`x _lM]tunv/\cIt6tid|1\?" Y=%Unnwr KLfRռТvfQ)<LkQhzvlbi i`ϟws&|O1%PbraqK%ZuIQ$uDb E1\HBN@.ڞ!IV 8Vk\Щ̯$e* IDAT?Gi>2v6NB"qZH]SפgD"״ʉAyrbdB&/} |Ѣv Uh]{Gik34Ox%WTZGx ѪJ ?ӹs*jIc++uZOАjC.ԆmYPƎu1wÒ8ȼLC5p7KTWZwԆ.Vޥ%yk%[a@J 1"͞ݯ{qI?_ͣ“y$ŀotZxOkdA0ÐyJegKR(sXƦ֛mI$raP d 1pϭTC|+XxK~wUEű]2~T!7RJf&7Sֽ; Q0 )VIeru dŞYw4rߖdL7 n8z5$4vABPuz"wub0@$}]'vW~J> .i%A6%viv~ ֞DbM!6xDjf쇲*TWY)^?*gP!]dVĖ6..͙CØ9Z {}9vNF#]O!?eB"; }aCjJ{TZǗb:-ztK+ܽt!—:N-?Q@@d1W1\RZӖ w([w]ID;(-`%@⥑ TD @X;aB.=5DmBYZQV@.H \wt ށ$Y>A,θ ؆Xes #XtϾk`A8 /^}X+t=*4p{zfm2e6\^M{k8,j[Cw%Fh!sCZZG%sE"?:ViM'!XaHgsRZcVtMlȗTlҺ7iuvo' >m?#Vjbz/e6(dc'b 6P|mO 4z~ݭ si`KaɘG;BAB;^ gt&5!0g*s^ӽumMO&TRK;ęφ/70g?Z,"mdfh)laXhԖ ;U>'A2EyjbN4].gclQtƀ{H&V 1L#NFr%ڹ.[Ir#(tfL"pQSE&3on3ě;}>j-j͑bFY?vx7zgjp01!orδr L{=Mv,\+̢INQ4 "=hkc:h)ChEh2|UL$GPeXL{HlUҭ^~<45/F[ov^'r"> Эxu#h݃ex4Xq&HL %Zf|N>ܰL.SFsnqסeRмP7=4gWVLD04d)*&K1GmiE<6SגGiQ<[So'W ՚dC38jf&{XRDkSFcQΧ`كHrW;}:ߔ6"=Kap[MrMTgԬV5kL bY#Kc t=#OgqpCĘ,m <`ůe( xX~SF#b>2v&FځZGPȿ@'?GG,腩0L0kx%ƴp)faಂ"ay@9ƨѼ5)edp*aw4PNRmr14rXtҞ'sRp^t&SWG7BU-i{ڄҭJo\HzYT>)u|]]tnl]"fW~ :1@aBsMc6)khx:́miC\SiB;0)\׈d (WRj B饶l(AKd+X2s\WlgqvTw`5QS'vOdڐsx \Ns uYFFjRoxbff:|8gց h<ǁ\V&*f+Y[h$~v(@ 8lΨ Aݗ|s.?O!`L\3C, ' ܺ_. o{uVi4\U؁~X}JP}Qe]ޒ_Rz;y:AsEIFQ6gm_ YPcwKYFHNɫ:v;1D) B.c=nF$Ӆo}d|M)qz&30^9/E`1±cd^[Bp*j𗻣#YI,|iPksVBbύpaXXD/ʜZ! :j8>uM<[E#nIBxCuP8:ۉ_l>ڌPۺ%x@aփkNh^bIK^5kM"%3֢qvR&:v gm0xV錒)A$M̈́8$Gf|(`9ئ\Z P$G|WPZċ*9Jӑ Sb!qKN3:n[+82 |o6b!sG5CGI0߉G +L_?Z}^]9{t_nWcۘ 3GT Y`^XAr7@o~L4]ܜ&Pzb޹y7G]EǶL0 զ$nܚhZO[րGm!;Y ̡hN WnJN%O!朴eh"t}?E(;XRRmE 5.ɛÊ,`yr}C"t`z :ZnBDu`_kIH>tan+Ňq=u7<}l2ysVƈRc ۋOk+$UHɝW-ǁg[fyLg!a$(\h'ؑ:GawJU3 v]'hba?Q~Y~.8+!۳d˚\ nBw4_\U"fN)[I3@?@ SD^xiN~|'KQO.K5U~?1kJZ$RK^ [e~SSfSx[ڰMUޱE[_nS%IJV46czH1P5N:ZƋZQ42,Bu zԊٙK4~P>:gɿ@tfRSsIW;X|ի[F'ZESSyf)0q8Z鼹>jCpwBc%>aW5]ikUPNLkb^-f+&jL{pq* 7jzqIȦ7ܖ+䳻2VզDںKe lq(2aV69KMOÄ}?0zm<.ۣzS$zudny5h9pM>m G6hnP^gŏ"NNM2Ąt7v5Rb1g{5_ KSwC$@[U/՝m$By  FJmX=X7$uΈۄI]z4J֬Ӥ*p%̝9S&,&$=wl;QŐ;Ui)QxMW҂hB0xd +:~Tem?mؖDfm>dQ˴11=Es p0%5Q󔂓ۚ>]uީZR.:6rIOi^d8R&@膪WFGv,wI "GI* Bي¹ͧ4Y2K\%11T͘'R>;i+reNItv]TtGGJ#XPH~tg[,IlU`?9.Yx/r<0.oYdc%{YaEyAd -'MOFME'kZ]?^\fkB`nκ;]=̇aX^z_M4:+\;G}w~Bleϫ= mMCF'fgJl@O ᵚ,i 6G-yh`hC%k1ݴӶi8924mh#Jz/^Xl_d'qq֎n̕;M.֬G2ےlFlr{]"2/B ڰbGβa/9Q4^Mlo?O[ NWB0Ā$'FvJgM Da bRۂcbi"25"RϗNXJ>4 ^RrV)Y\+M^PbXR8Y{BO.Hͧs9S൲W/AٱL+ 0xi^\ٲCTJw ʩ^4eL:~6Åyڔh+9hyWAeqp̚_ȵ4ٱ4o5bNQ)w"_5Fm;P0p]ؠ\C Õܐ(\U:븄30&t שuFW$a @pvS6J0=ݣV\CaH3V8\Zt|O4eMx{,-`>TT MT#ȹQìSІwEQaʂ 8sVKt-rܠ+[F4ՇYxY\zD\DP,mS'˄*,2Wj3Uz6F|$jo{c8z#q|Gvkf'WQ{V{Մ ˩D4<;/,84pQYT)RIw_Hbm9\`ZIʂ+L N IDATA'geƱcC70zgAzVd>Ul:֚Ø=Wg gl(j~9vnpH#Njp^ 쓆Mݭ=MR'8&kf d(&ZW! ̫xp3ı)X!*Tc^Ϥ4f"3jSngg W\rM5'}]@DXKڄHMH-fȸCs,+2!K4@x ]+h dx)DҲ#&ncb0=h^lzZ"%_3V7A֑z@Lf+ҩJSO dM2t=sbse]ny]lGiV'3Wxtw xT4ܭ(I"Lؖx+pYIKl6!PSNe<&$Q1ayjq&Ĝ9y"(ވWIDgzes1oOyXÍ<'|TXGB &֯4"?Y!rr_אe4hOqwCUM{jh sRo ?K[|#]2Q;bˋr30C K&&Eha7\Dtܤ`>< Ţ/k6;ת93BڶWX*c,:oD^}fcxpfQYPs''u]ta^ݻf_,1Q*= ~Oy>ms[NؐkmwK19_5^գt x'&!EE,i<֊a}y\_>Q<-fk #dӪ~!Z qܐIaJ3L8q*GmF8QI-X?Hߖ S7KTI)Gٟ7:0A^5kI eOLRb+Z&:_7n6ePK6SǘPAu)838*:*P,* – j/#O-:`ے0QJkM9h$CӋ32l,1+9^/ih3(>BGS_Ӆ9.gǰ )dv7wV0Zq4JWf؊Ek%&g =t$3toAu!ͱMN\o 6"x%.KjQtʱT0¦rI &QG!{T;czu3FQ:n5!D &2ۈ%^2>gx2[P 2X̏}VT_÷}LwbR8b,\T4ǀQD..s 利-Nj?&ѼjeIPJ!JDH_2y1˝{? g9'&Sz]bhy.S"kOtwV&ֈҒ35vOП}m5ipy7.V=ӻItq;,Vغ Ӗ5<\ÝF>//kJzQCp[L#8oZurSRbpsЪFǍP!x FD 1= .D1+S)_pIbk j#Lgf/9PgMj_{ Ӛ! ٵ&Z4Y<&d`$x(W^PZtXn(2:NSd"v%I#A{m$Bgl.f_ok{+sVo3m>/+ܲ&k>!]C{bB /v/j;ڱDaݍ'!iZ ,<,l]e]ׄ;V 2K1\'ۘ&tV} r$xkOtF^HI%G,4}ϗlĝq$Zi`?,XkT=/OA}9q;[s}@Cb^)po_̕JOpq4lgÍ`%%q./jp?+~%TUg&%x4 =˥v,QzQ03*xFz 7f:9ِ`h^ExBѧy=|Ce;wa ^NCr}/@P66Smv\hXJUwٳSΡHbFC{m@9"ņ)^|瑶0aJ ^S$ mF]t38ʳf\mKvL0X>WWSC~ڽ:v,KDX[λ9RY {t~}5Oŀ5x :/#NF7y1!%y5 D(GE4PL:uKے>tfXk&ڵJGIA)\a$F~Mg% U-`ڪ;t9m"@ e[MPOlh <ΟQs'K󖉀>_Rs28@2~v^~4 G61ҵ)"v|RjX18U6kN)NdAg6GgSx#/{;( 35y|qJúS-H:AQ0L{ivp7o,T["\Xl\c㚇1EPmk[iQ,Q?;T?[ilގlo4N3 (8l-(v8LyiqԍE{֚ {6`l@43j09zY4eⴡ6,4i^)0MagG:pIitiD^uK5'쥸s{WNj—p7&& SrnIѬ## 8m\*#&G]6بwl/Ѽ]MJ O8%h `t9bgpxTHݳW#WY1hii &7>Mc*G~*WZeo #,BhpUGHX嵁 X` ϖg}[OVc]"Ԫ וabX ݅|y$/H96$U5Hg韥eͼ_`(6v/^z6!)z/q[Pjǖ3>]5:SΖ.[rM"5SL ѹJAjaR pjfjJH.SILcnM>bZ[`'l)Gy8cOtu;uh4۔QLDY $ʤ}&]wN'5JmeD4a;[8&W'oF,R4,g8{HGm3h#Rv 5^0C[k*hC_h4 ,|Gne9 %N}?%Hu1MŽSQ^UNr~qx #Fk ,8Xv~) #%'sF#}g͚WULe73\B zظyи*!jb,N `d™Oj_p/񶗱9 RuҌj.H S%lW3oM\`|dU}/;oV]gkϊFѫ38JbfB$$9X+SÏ_o؛tXZcCCV| 0vױ;Jğu1NJW,Yƨ^PY^3e͂4r520$qTd~WQ&#hUttX"v}Ɯ7 {BRWXyjI9y8BߡF93ir?"UlZf}/ԲE7F҄<ȅ-lԢ?k6wOA~#%G5y (#Ƨc|p-0cEO4|ۋ4>XQLSeM* 8%pWtl4dy9%1RﱴvZl{W uA%~DN]VeKYYl׿*7Z+{¯#k]r{!3m4s'˕S߶h}~%VRt.)m=un 0q\MBHX ^"a\gcģڄ%<,,wm2k&L =v_ a?'E >3D¢SnPV)8 }qc8pȮp"ul<a bP_٪ccMsK VT={T97)NӔY؞51$?U&%⏏G5)cO&F]z jnm.Ŝ^!fWsoPy< MNwkx«a8tl!LJSWr0\.ZmjsȮGx2Is_iퟗy=KR-y)Cx۳>MzS}3@S{>ð2{T IO39F7&w9J?+{F[sI#է([%G|ܲ^wZG^} \9mJBkDD/<6Z~jf5'lfx&]S$o$SɅktr5_5E,`% 1jr͌ Q; &>6l2/&9'4^Ҙ-Ec0%d"Y3`5RDw:}>}Z\Yr>N'k&/sG3ֈq*oKq.i u{vQm*]^4*/oeM~%kcvw+ĺ۽jȋI{h+xDVmqJo]˽=S:BJkHt.H6`۔F> $MSc{qZ!9IIc{ylKĹW۔Cmx+7qu'ݕXY(50P2Uliz9~ [7bcᏏ:1+ ~rwV&uwL&HD@0:%\vbj{L8%$H:ـ^c xeD~6%@vՂl`gTmA:⮽}}#WݝؠkTKy0r7k[2NИiDy0%܏uD80&Ss˻',̔H Lv}j(_քUa9kRt5n;SA.t/@VK~ PQKL|=;3ԫ- {mVC?*:9;z Olq} ^G` >IR 8r;9uOյ_r&O%%ߥS;.)؄5&lEÝr8ݟ fcXR h:F7E$GpnH"qQhH3?7~Q+A8ˎt ) l^'RT1h݆3N~fC=lkkϤDyXZ:kmt+uw?eBv]^1Hw_{N)}ϗ<ß6xvS; :;I{zI%z\j @.=&in /R딎{ql<:c:ɶR'ٟN~4uл}vK_`G ZzVYm_etT(.DgEQ+7{24'|gTfŠfzN"r/YO'5g[9l ϡe3TD`hYlr8[OlvWݦФ8*T?`Qo잵qzg8Y,˭!$^"^~Wthr[ViS,,J`Q{ =\VU8YR+\3<A]s"෗'zrx : LEAdks~6nlRvTOPV1P,Hug/ Ag%zDXa׺Xab!q-// ~?}*,cb>{w5pE< o|Zuz}2Uiu]1('?K+9_Rt}%CtӅOnIueGq!>q"%wgxа:Ji@.h|+9)NdxҸ%s{[lTo!UPL4|"UȽv #9=eHI+0xD)ZaAߗi<5k+CbA>!'r 2nGſhX1G'$OؚR,(?7k^:ZRۻŃ >o:~&X]myM2)Ǻn?;OF5LȌȏKB[ 3φ4e^/9^jQkNY+^IDhXda۽wTw`|n0io_~(T[Ωup~珣h8*>mC-xb"xz4͜O{{4} =y%-M% Q"q]u|XYYS!K$@'5^uG$Ӛ4$r4 oA;@LrMgyv~i@`QQ*-zR_"p?:b>UYں&s ڋK_"Fۆd ;i63<۳5Djkach:Jd՝6#-;-Mm~oo0-@Z@iz&2QG{z] .3W:W~uaU|`rdvi.6jQڄ`qK>u E|=&G.Y.r ^;c6e9k6tJGXh]`:"lGg7ClQ"b`- LF&p*r4Yo=s MH]`530Ф5R GWfᴱgW^~5蚭0Za4+z,.ZPdB n 19Fb?+(^h3]bG!`ZۉPe Gsgus*5I7o(.~?*ޏYw'ڣP^ljЩR} 92.]+F%%F|N1/妆>m1+/72Ovr[Gm|v_V_Y^<mM8JǝM ᡭ[Bk9քjcҌ5IG6Fڡ<VDnwiې]dh1?,R\9u9/QӻKuǣ9[)Mk~X`DuAYH7u,M%9L&4Nި!:h.4\ݘ{٦k'!{ZkoZK%%ᝑ0璍 FKV磳ݵ~sYoЄWuP3Wf 2O|~^w[M _.7wMڀzjᚓ5>'g }+X;zذ-]9,_9^zF׏K FWb_Wj~uh" TGBEߍ4J3\gBhB蓴5E Mek.>>Q^K ]Br;w%Wa֮х4nk§|Kg )U;A)e94}_}J^n`&׬u3~|۲osnqāhagQ;>]"hŹ^V;=mu),ĺ.E3 G5z?/.&{0(JL8>LGm1u/|^d p ]rE]Z[A~,-> .dmcm}i!uIv}uҩ="'y)xV+р"M>5GM0f]V֗u$'em>^!صhEKsSز*2Izצ&&Zl#.8Q{(Kzy_n.D햽ES!5?8i,DފM!= .RoԤɫmM;WD6C4f gs\dF&UkyJOB7ʗm^X SL(yeJt}ǚV $t^ 9,h LtNo[u1 X~-9IqԆ\9sgM l}S_AdiN˒z}>ޘP'ѺEu|XJ3XqS~IN Eij/ Ea9xx Ekř?-d0{5b?cyCGx;2O lGJ`^qV3H&F[>8>mr:;PJ!:V< Lh#kݱ9Oc>*g?ZP%Y4/esrxKmo,-ᶚ>Lyb򘟔9C5ĸ-VdqŞၫk-ŁF' xxk(Vq"ly͖vu^P08aR<8eH=HaG@x 2Mڅ;=w$'ʫsI M V8 c5 FG)"_lN4:0 rĩtIb&,kQF@"\0Vl$W)K_`LaaTD[o_W3)ޣc +*X]@kV*s5|I>q.RIAS ߮f?jG <⥇6ӚVIs&ZieGvJjP5oKĝSգs&bmqI6џ\^ *L>O1؝XذK80|Vs;ڧ_eƝlh.nrOlL zxm' wj?_2U$qcMۊhDx0DZA G/|&ArM$1K;۬vA@ܚ^K{`Q"oX>Nro=L3l2mٺGpMk$LؕJPu;+m< 2GhDqT x*@(*3q},#*%`jZ829gn%̒K HeS0[<^}53*Q̴jHPg du_ṙPp/ˎoi^Hjۿ+TEE1FLu ֕lkD,dI)"xfi!O K^F9jGD680t5dk}CTȵh<ҩhɖ.㟯҂ks2팣pIti|ԆF #\tGixzp]mM,+g^p?Uu4$D3ٔE]^Goׯ+~z}|WR4=^#oR뜜A>9cg/g]v1zMl,1F&~$X@ic>[4-\LK&~yrFql\a5"B$u`y Vup4U0&ϑ`|qůfGpp}x(gCkS:5!5[sքt骼_g/|&z 2԰{mQ\C0D R_(d柛Qd_6N-q4GlU9@R,V*bFzӈ~ԱC #M}52ش@LNg/Cx쮝Jfs; χ鵚 rK3xO: iĩUs_W`ݻ i2]L9xvz}E\(9.Yi3w,!~ueVC'%8ps&(趶Laq1G\ȳvF}c`~v2mw'WȕD""* :YgE{uuލ%\ygnj⚚fnۘ8dh?m !vNltUdNFvC Y t0q}cQ>_0uO8ar5&`@%2OѺzA}Q)Y;N'%:~"O{.C65]gց}'!.@{c=vGf8 ?JxpkK Szޤ!'GkmSuhS4Dt'a`l3wJVb5 YJLsi/#,]W%N;wg[hm0mxaҢ>Ea}es-g9bY+4W FA0G9ɭQ *a8daiUGֺٞܵ=Xj"KbS ^! ,}{AᆢK#K^®5Nh0afniȉ:YX @Xu?5E7o˃Xa,fv{T_֑7w"I}RCB"k8FLg>wK8e 3K{Oǣ7dGYAZYNdq'ZKH|s [H`SiQp'م(lĢ9Lmq29Cg`3,)zlK?X>BCWmҿCʙ܇ ^к$^ y+¥bpAVsZFǿl9?--qLE BOkDm: IDAT PU$BvM[VğA8 }>8-g+pqZZ_62s^VӧH{lΆi9ݼѕĿVNV$]w~>w/ |㬖 " J5LjziOʎU~ݼFVrsf%?0 ~;W , Q&Fq?biFv8ߐ;˯ 0\EUfEφߕRMir|b4q|OwtDvIAMb`mVBRrĖ-+P?5qL?WCF3VB, ⭦ <9aQQuظ#8e]hjsK@Lf6a8%s^ Ga-i\VzD E A&)>O5نeFE {DI.~tX`:qҺ\EsϦ`IvRZR~TeջM`Nı_b,NejMQ^:6%{yΔA_EĥVMU7u[bыZcp?+RS8P#alkXH8L49KdoT4AVT ^Mb5zɾFL Saۗi"_2SVnM"Q+ReVa ydxYhJE ϊxy =Oپ))-|pS.Q?On,73 Ґ{xHelB߻dOKY]ʹoLAEgY*vбv{.jILwve(aE<ѼjRYkBӺQ51 HADB_SuY'i@qGJahU!Mk"LϢV Pߜk$b<֠k]qC-(A%M_ ~ԕe&PRqV Ǧ9i]X(~>)I`:G')D+7]hYw5 ibiOU<!S*PRtYPu.s 9,g .h#8C.?uLhY,3f˓0zJ,) 5oFxΣ+Zg~VFQ'CUЕs+G5L``iX|d,I'9EφQ`YSm 7.\vM\76F!%PQ9]V-f㮘1QQ5{S ;Q܏1]4ƀGu!CYuGmŊД0{Nj0 Dw|y,apnG?L //ÿ~mCNo?D ٴ}oO9{7}BX5ϠͼӼ$e&F~< "91^"،X`ӀNy[d+Ryヘ}Op8tIjU ΆvQ]/4!ר5H&p_?,\r] ZãZǁAاQ }LH^{O/>kjK>i8S"ϚRKXKNzD:TƫZ`*?J3#R Їm>Z%35_]#nW[IL~ \oJ:IѡO6Y;d]3bC<@w݋vMdDKWO&Px7o"YN!hȷ 6Msԇ /. d\7ٸNKϖ.nuΰ[SBxT\'MD͡-OF`(Ӫn< 1FG c/cu!KJ4ZѱC(m}s2E4㏳Oә q@vJ;7t P鰕5*3H$<:?mVVFikcOUZG'adI3}0E`CQv5#FwjVqjyumD:6 ZAMƧFݺJXMJ bKK3ԥ>L9uIaz^49[CiOxq~ZޏG¥u/xωM% 6B2y%"@+\*]Yv/ z0u8`Ŗ;8MzG_eJԯp"x U)ۊe7ȼjcwr4TXA_&$vhC?[;9$}\%[Zϰ_]ғ֞zD>^Q9GYᴜ uwۣ݋ s0oA"WvTI1dҬ98ŊY,Ab/Ց;C|&lB [в& u?'tl.6`К(hme\!;ٴdZ{2^\?,Q!-YS  dYOt\^)՚ZD% jyea6f:sV|/2riEʼ7 `z9 .@5EFlٞ7fv<U%6&[+9L'/ >_Nb^mzyM<0s#Fj[Sl"͢@m&'ȕTkeל9m<*o.)[bVeʽev6l\~ aJ<9_ky9_]:rⱘHc0Fm(Z{vg*Wyjuh2"P)e掉\J-SӇזifF狕Yi7M:h1|l-kBV$sbէ嚆񠁤zY]9YR>5fB@ѸIk x{4_rD"5&8C],@|/ •T ;h֜κBpUaظ+ ̨} rّrm.ݖ4$s(8:ESƥ'N@/j 7ƈm<  ~gHf:it)g,шjր3M;Wrw0]Ή 8ňK 5T$v)Sٌ^Mֱű;KJ3Ds-DmZhx?-j0ZZk ېV%:kw&h'dn:mxQ?]9k6t3d8Z~YC&^ Ӄ>rįtF۳ƾnN_nEw2YMCmQ+P$F܂Pzs 'lL㓡jR^֩Jza|w8~~Nɬ(L'K-)$G{L96 ):5)-'4qX!)bcp&"X+bF5L89ߋ>JRnqT9;?lݢT woI6!Ѓ.)*&.ϗ;; gUEPKfj,;ޏ23Iz0\mz>?u\ h:̉VzZ;ѿLi/_Va덐# _ v-kB x?|؊`ٽ)"n^X4XQR Jl9Ϸ4TDh4qb4ne ʚݰt8zac?W56,)h=HPF"$2W& Q A%!MdmIq [sBM[BN[tQ).W o>mߩge©hxШHw=IXk^@xjk~LjϝqX)̋RM_M79~\Y(30mO;1|سuԆk\bיڟP+g(Jz/E)*iv%îMH-c&&oxm%? je\CGbK 8IqZ>1II<+1vI҆N eXb#Xx2eQ-8-Ҹ͍*2H/U+ZWH9꾶qh c$m K6W}}Hw'ܚK*>`LMtMϦ\Y hDZ.0,30ċ|+VK+aSwtLkh[D %k"Ap"b%@ rQ H:w8a2Z)z?$X\ZnA;d?,P/ـz"A)Na+;ڌb X%ⴜ̀"kbR~e]]8gI|/{v|Y pX՟!I;S>&7(22('?Kkd;6I hY+p{&)h7$`9Is%WX؜'SMY4'pƫuԙ倳'¡zM} 5{WZ,F5!X^gV1̽;贳}J;6A0.'9 7"4G$\Rx=ԙiKH&Z%#`*gMĠFcfai9z| +O nѱ Ҫ1Tx{4}f|-~GI YvcƘ%SV_Vњ?fUcP_iy3ܦnYF|$w1g7 ̐.ܐH; lץjMMuĝlb"\/1T#%J`%*׆aKK,!:ק+>GD<&pĚҘ^+M,hc2skDi[ڈ<:j&Y \!FJ"a@S@ ,bC8Gh Tw 8}:9C칱?dIo&h}P黛ג'@X %p|]<-QCbP3 2ugN:0LL6z~ 81(&N92wU׊t:hrJ$_|v1F#ԺsVbl&ִ@k[$f2eI>I,|ā鰩Vuў$GZctL4OfT=W2 D1c##ӵ1K][ _W&`p줠-sdwjTvjc!wzIcң[+ɕ,Rıw\R}5P?\"RiWEyJzP{iMUq_YSo 928]pLMtS Il ;N3$5R+;MqBў/B7Ï'TFw䖆Ymzv 85!p_x7!ʕGXUFR+plX1! 3 ma~t}<v1;5P^*͟)M}yt?; bLH?oޏPXwƱO,7޳QLfk\ӗV'٣6et/)dfGi˿K~(͝BE DČ4'j*!@T2 owEF0gM'$ :/a:[ѝ٤Q GHAytAJrQhBME%)4LG1QGnQJ.8>bg_~+p(Ͻ65%EKg=P{ւ~9Ê]Һ0Qn9=qyKhD\S8=4\c!-{g&E٥n /F47~k xԊ(T +o[,;sڷ$N%[-l>w$gߞA$xzg7 Ж]*iۅ 1o wUDIC$+ ]0!kzNf j&T`jgzT`@܍Eӏ{A;~X?,8+ϊ 1b>At`|Q]2}ze+g*b.&S>џu;u]/9!Qkz%b2nk: Sh.QXEӘ9&ӶsQ*φK2]bMDӠ֪tmѨMz XzD^bABX$ !jֶt*ZDs,E8&#i`lɉ(׆`%q9ww)؞_N2*qp %#a$@5^b*aZ݊ q&5s)҆F<>Bg_i&֩CVS: ݊i>k4H$XҳSFg}]BZt.K#Jݳ?]C\ຝtGlI伏t}\hQԏ{+r{pi=?[]Ud|q&c2(bX8=j&*)h:sߖ[qjBFv;x+DΑ %AۚPLyiԼabB-u/ls٘i[OEnVR76\{~9lGB>8VFiGM4!OÀarK6}􍶮#/=F^SXVE \?&~T|CI,ai8>_6s >*C27Hyo6mKq(FYQ:*C5&iZUT{8;X8Yࡧ[{_F>AOkJaioځs-;a~U;stͦZXt)":s,p[jt@ȘIG XTV޳A|!]8Ixn+d$gGN; V1 svlJ5hjq67sjK2M&(q]vw> ng3i5^[Bd}xE?MlEQ_)F2v[ߣiwqJk{f>6`ƴhQ#EMf9iBk(Gg@i-9i,j^5(i0]HpVHWˈӶ5r;]V]|TӮj3Yt+ c4=l=MVM2v׻{o7[+^ϰs GQ0OSt_s;^^+ꝍVZg6hQoc~ށ2En>02"U;<^meYfNrFTV?a zV\؅e`0]F\pE D`֓*iӄ`m_UqqmkN_iḷ`#p @V[ɮdQ:Й?` q{pbqFX#k:ܬ$}!X^iK5t :twb d4Xgc.|M@n*5ECo3DyAg6DJv1JFtR^9ܖ9ȵn%8&uNny%Aţ4Q.d} fS\t9% c@CW%тV1$ԝZSa 7?MrS4wtKbkSwgbk5dk_5dwخʴɵwVi Jn[m ~4:n4`zOG-+`qM/,%GsgCJ%A}jX1k!8m:7be@ #%0L5M;Q][W=&Si֬ 6m6N>Zׇ5 df|4,Lx˜ ֈof]M~bԛ&>UÍ5*ۏ(S.jNwVGeB:DD|9,>W "C&'F ,T4n'ju.:NPF4>q& ;Mt{˩38"2L6rcҘoͷKkNf.t\8l;8׷-??N_Wnt{OՁ7$(i+xT<(^M0!Foݏۼ&N,YS/)}]MSt J5 /#*Fx smC8.Q;bSF|<ٕ^~Z{qV+ߏf/;h~CUnKNtʙ(,xpFo4Vd @"AZ -5'S;FAED z`fO&+lGuq%SWt8kV-rkߙA۽Rbe0܁]j"(G(+k1!Y*3Vu(wP$*מw5xjp?봝w QK4Ŏ /KzsKe!1^U2լ٦Zуƺ3>EHZIjJ.㶞 ÅO9a[ x"8LVq[V5;{1':%XY,ܦ#pK 4\4!=Yktw60!~յgtIhY QFNjwEJdD5E#<_V|ԙЦ{?bDL-`G#nşeYVi4O~f@/d a8[93 aq"t)GYP: Ÿ)jQIx*+M/mkc:7g:6O;e?37OZDasy/2)!ݘYxL y$˓>y y-〱oEVt#AHAv:ܞu.`36&m3*d8֞Xsɥ[1&9ad(UNAnԄ,)sqQG0{-cu}/4QYchmbx ymu4A^%J MKaWD%A`s+JvQZပ.yW 5@x /5ǽ ʨWLSl^jBa޷%zxV guA.ny Ԇm3-ֲ!*G p|ĩNr~ZA钰M98`j%9`*[6`*/Q4]\Slkç˂?R߮ tö`fVt50+'?y)CjZv|21?kȻiʯXHڡ344zʋy6.Ys֧K8xԕ5lXqA1BBϸ0V^t3w><-aהzֆKQl:D4mW[ǧ5"}[{q(ga_76+žo[ N*k۔{P_빢lC ؠΏ8Qcf:3s[Fi /##teo)~ռ eM%.jSf@m[5;Q8G"V7b7ih]~܋uyg ,aU 21YU4DVDcؿSq鋋]#z@N0SX4]1fաҹ5 EZ߱;`uo~[A\Z8]\ZmB8#Hg#Υ r@4uSLrƓtI1N&@?]juqGo]PN`LRu[i=9G4{? ;PrEHgۦ5"D"IwG RJG% 'G4j6MoŦ#_г6%+*>D'QUm!ad3/zT1%qm3B*F!ݵ[:cV2ȑ,ޏ5a/Cye<H,ɕ)c+t nz#0@xaVdNW/TT W}[ $a{\'_Fc2VC{i~ppj.aP,N)[;٦;Pe~Xρ˥ L("{Oo1ӅO^.хណ?g~fl.C!%^. 6 %FOЂ_\"ʿ~%aiea,6 :)Ky?.ؚ_"Thм-6YQAm65A@(CNg ϒ5Oz.KF鉪}Xݽt|bc-ABLL! M-HO`'Np:л;לeʜ4Cgݽ2[.SB躌LL3V5blbXh\=lEw[^/KE4x!{[S]}?+qö4#SSJ]:kqF6 p0l3p+ mO3YY!axwקh+3yG(JXj[Og6<5_ 䈃aW2;(L+0OCŠ)q]@0\ V26 큣a-4Fi&G:!EKR%*r"xqN֗PcMR1|] aN1U'`k@c52Uz躞 }ѿFޛb3ダŅ:U9&[sNȗQ,&gyĩ(.R8`Qz0дC58<\̓W E5[0a\4Jy5-tSڹpi9:,ől/JvH3E/kJJZ?W/PäsX6Ghh٭wclfI;(^LuɎo`.Ec"`??GNOWK$@O؈9 wԻ˚ >HvSQ|i~$wvxT>OIz8Lפ<\yh IDAT:JL0\l!7PGՊ|W?]\UQ;2bTسN[%y#3r-58qsU:u^mh4ٌS$w*jcz$͡㖢DךmFeMh?mYϗ}W wN*]&mJx? cu$/u\(;*dp]"vVt\IƁ5xIqJN P4Y)#t/pMNw(݌j6.$*\RN [4GshÐ ږ` |f?!'Xےc/4(T:GEB)fƒ'ZFyIrϘڥM21^SF-)\ hvnsLlwBVY;"+RQl1Q$Ӽi3Rsfw_+:UxFCkM&x·#Wr=T\/ P0f5qS iU8_tFp.-9k1%WK!۵bxځG(9XS>YцU^?_m]94b's&ٸ&}B<8p\U.'C C_|*Zs|#'h% }+Ebh}oe`kI,-QHyLQ1'r>J15e0]`Œ}Fvƛ?7hpew룞EF#& htj':uǨlOz^T6&htنv[FsܥY8s?i$ IP{_ A Ɍ6ٝk`kM@ P`VTƽljbBթNd4@7 0fWbpݢZ&c u]cKԊS. \+؊UY2a.^66"{%̺T*Y# 733"P5&tX^|d?7dD~wQbD"δQfHnG3y -PQRXQ^Ʊ7BeA+%2=MN*(6qH\d !E }͈\2 P|^Z=)gp'ʛBARM˛6#<Ƒ*-P=rʥ P촡HY!-:ON*fB4$1a" \dI?^d*>MvGq&;:+'ui1;3*9R.HaR,ီh}c'Y־Bxk*\otNwj)J /Λ+N삓/7'?PD֐64Ҡ&&ڦ7A)k -"WF]NMAZyChbCo?CN}~U?+M'Uhlfj+ Ϲd7?Q4ߺƭ5G8ycRu﯑L1=i>WQ:l5N_F^mӢpSx:Y?',$u_C/?T^ f4,lrDyZHRךUlؔ0N *crxo+&7Zmf~p`ؘb7+,=̬WkU6L5:$ybv^v--EBdDg˻P(NӚy1xZD4Egb{BTk]Jm:xOHpM#LXa_]+w4k"`]x_X*_IPƣy;%M.Z?CeSe#xnќ\3-[bk/CkH[I+0!B=kV=򇣉+_ʕ 0>(C_MEDSKֳV:gښ@rh0{RSSjabm0"Wwn*r>o :}% +r4nXC: Hy{7}[\=P9p1h8jFgnߛ{jLxaZTJ< 9,Ɉ%"c;ܯ2sc=cbLx88G9oUڷ2y?bsIo5Saye|aȧK@ЉJr!̒Ɲ!FK%DI JCh/ ,rFhbTy([ xDb)#Z hL9@V(|K)7;`!%Ð* OlMT{묄#'?8( oO7~wDh8!Si^BMLt_M0XlI ܲ2!Z:XaA+f&[SdaT=7~ՅJs<'%ԗ>A L(54Jxg EiL=&`M$99c.mU' 5|7} 8Kuev7`&ӹ9J\R)?[RfRt&i>$`﷦95+"DL٤'L%-i)9]ݙ=|g*܊)N*w>[zR[#2Tмct99xzQ?kql8<\WC*I*_YS8v ReR XnTxNݐ-rٜv`{+/KFzbŌys`TUj25bu>f}?oDW!u11YugF{W Ҝ?|'y7C'b*BEb7ئ+vc5mI$2TD>]6I~{7>yo3ϓ3~:v3^M^sZ0Y`7|ٲ|n|M` |6fCKU wjG !jP8C0-syY>V3rMvӕ<7g31"RSՏ>#oun^=ʧgI kK+_~~ #Zd9x<6P޼Yl=̩X&bc k MybvMvQNGAj."/ aӾc/cu?,<0^)>r'n+>*GD/_e؝|ެ<~VL=bc9nff&ksDSn"Y6("uP6iK^{>á]_. ȎkՓ_A6iMA^J몾Q.5%$9$ywꊏC) }tﯹ"?:"Vi:BEel' {6SHu3眓NWڕ!yYF1yIr<ݳr{'rm%\iYo_͞_A=uM fQP)c-Œ=l.YnK!#W~5: ћ]"l^r:9apɟ S<7ں?uQo }\դ 2»ߏs!rSﴖ7k2@"l)jzCTLoU^uPǍŻF͚3w)A,as``ϼ?CjAT~M,Ncd5$dݳ+2g߃D $.C,0uIZmBN[6Hr}ӝ]'P0qk]oͦH-%<8ʓ1F/G5ل{0 ٩p,ryW/ۚZ{&ou+4o]@9]O}骢p (B6 e ם0PD8h)Y=# L[u?x~岨a:5oY'CQCBR{r|A-W^ɿs2"uT0f}m"!iHf4c~γsek;DB(f445-i˕WC~K)DDc.d?F]Ka%5EB@ԬX;( \,HSiKr~`ӨkzMJ)Esd@礮bS%14#w^bxi2=g |+ܱ &05^F0(tٌx3{$ty^ЌQ\Wt 5 p52S`:^N JG\иE- ;mu8hRy6ٲn¥= 9 pn"ĩgۼeIY\pȜzk{~x>nqꈨSHԷ\#-áWY67xPEc0XXu30Wph{!A~Ein(`ӥ7~U;ɓOW|{wg[mGtJ!D<];Έ⪕Qn<@Y=8dG p𮮺:@7LRm r^QP AzS184qP&rox}#OHCIlƨ$[bQT LhMBhpo]oj±g\S*ϔ8DLqu,z69bpCCt](*!߻E!x}f ^bO< aqPCEr` 4THVT(gC[ *HEDN`;su g(]!Rˀ=$_P-饃K\:+O N rUȢE칹 ʳ0R(sM}ogQ]Hl6 =wDyz]wz]Xp"!vSTo,Mn}g=z/s6v m\<(FQYJPWfױi@T]uhX^p?| ޙ*޻~ ,VsrN $R8Bc4uqnVON\uJXqH80U#ae3vc{AY(' {mT,ֈɉ²T}x׈Ct^\z_ G!"o#E""@Πl z, P('()}t5yNUU7<9 !8:EO%U<(9R$L5+)ɟ_qΩT8"5er?V΁ȏ&LPLj06)뚹 "UWӭf لws;{Z\dp%¢M~˔p_[Im,o%"\"S>]]:" Tw2m"7 mPvf!|ASuU•ԝ!ymӅgQ*ܘмwXX?Q(>Ws X6EMm yIȲTs\ô;nghUFgh$ޜ+>u1*;,>Z\Q1Q-E뫂.XӾ F[.r+2 ! mu?yT<\*׎\%H'T# ωPo;!l u۪S)%횙C ʦۜI&bjSLTY3DD!`m5ApՖQ IDAT\$藻`bS>Ӊ5f^XJ-Gm^]CЎXsP JaJʏ?.')$[$CJk6JYn[U; .G5=`֗q$[JmkILc:RF#b<]'iQ ֦f1d3Ff<]&yW+/ pE+T>ov,vlr_gUMGmRh5GN ?g4Dg|!z9z0T4Y4ߖNc((s<6X|lԕel_W]?tfS/(ce[ Q/4)%*OɾϡFЧqQ]G>TE@uE:'y5:{T1fWytV|hIzmkGU0v ^vQγ6{=Gx&'4Ӗe5D/  R|^ mV5{zkU9Mm @xOw+#m]aYcfCT?="7;y^Ly TG;//ԃ>Lo su:#i&'ȳ""j1l%}Z\ !ZظpSjԺHyP-v)FQ١y^Z!"o{Gވ{ D~DvQϏθJрUauyU# _T$Ͻc#Idڊh*g$fi]!Y&y9nI򲘤|F `kgH/P\MȴKCBWRwU7D/G67LHμEym'(&ݾ&ˎ>X4}U7 $x%YeJl 0|t5PeR$7,P㾓[2xUZPz :WIMf:}j(\s^S]N]ñCn㗩v)tۚHs"2z(4i YSTΚ\Y3 ډ:vQr-CgnzXPYDW"3FPx;(JZՑ[#)bl 9q(}6S4|ǝd'0a!|Sfғh3=bߠNZ'tr( |6$L˦j;pS/!Zgeݍ/WU{ӊ%*H'". `Ԫ y E,4c'!!xD _+x[|ٌ̿Y!mCY.{4f+N!rÍzGj<mmq⤥˻}|}M nkBM T֓#r$zPUHU}%s)vCu>ZW37G!R }<:6ȗ&(tIž7;dƙY8\w"?)8*E/3sDRaisxhq{5Zct]C2bQDHB5-ǡA;/!@udl>TXM&j;R,YذKF=V20}rY\,_ob?bXs%+Vr1sVOid䜙3;g4DZ Cxq;~ba6sOz|eBSԁI %Kwg'z\O_;G3fЖ2AgϏ~!*"7t^wєxQ9TD?˜ &YI*v aW2㚄kr)B}^Ru5ArXi*du909x%+8 ukUI2ZXaNA]!:a"\NE؛)UFMc ]E^$G!rԈ%`\ XyrMK! IjlA‚t́ۚ3kCWL_JE%9!wәq*>&>m&y}T*Doԥ="_aEĵ_KT]cs3'pN9S$FGXKy ̈́WP*m#etPsPqq5ƀFSi|M@jh\r%s=t`(Օw3`2Ŧ*PfH酥1^0jM-_$ ,Ny>!pNă,8$Mc{x>6~š9˅'oټ);?c}Ȱ:\phy^$q- ,!pa%';F'#h\z @c7k6"? QcQa1}TD{9ꝛE-TDNc|Ҧ"o"֧1C/U.J,@\ބ~Etn-?sSraƫ^g,}/4 Ҙ3鶡k7O/]i5KldF1'r;Y&.\Gջ}'j;u?Jq&/1d2+#4NL2x1sE'MvBT3ѹXgM3s)yNf.~Sk*vX'D %\0I*CSs6 D53.g{r]? E_"0\s}Q&'8Է}/8DLނU [5v$+˖q>?{UC9E5܄gJŔHkKu^ t^_^%#`k2 ?+ױ=ִs:$RƦSϾ 3 _φ3X?6:Z]/"Nn9 MeJz]V#'(ȡ :od͙ymgE49[{ehq_SceB&~o`Ζ^]K#/Ƽ)gKGLQGm-.Ql}^}glSNsRgri&v![S6O: ٨eJSG0W%|mE׆" {]] Va- l<%rDUBa蚄Bx1L3FpkPtTA2v3iBuk{6=RG|h>`bFM xERY{˾n  KTKrfuǂP]ʈIPAH6&? ChZ_. h<qo;oB%-܍f [: ש0כOjedJ} 9ٲ}ۢb&{0F)7~8'?W52]|3HĔۣHQ[M)`Vh}ފyQՉʙX\W@ɯH6z'vMD˚á3.="ntbaTa9DOEg 1Qu9>I^~"\>| 9+oTr kOR,`tNJYTW 1˴__W TpDC=ܵEr eDNT\MޛiiVmbcx<,?{#A\cЩi)kn }ZȮwedK n3#t քQ!ͦYLd+]3U@_R!jOZ5k*r("*ި<94!xNC4l-y($㙊6J3馔1Ju`à$/m|Wx# A7QA>zꯘ22{~"ro%SQ۪wUXK?^/O<<ˊhq%GXD&Jf.'##r"y o%fKn P39*_$ˣےXsQE^j2}p) A:Df(9guΥ|M`e3 MEdZ"bj=~Iu\r݌!3OM Z<@z0[8(yw:bSz]rGfxk,r?zy!>\k*F7uA F {3Z8\-Yш \ lmaw1Ph@.SGCaxC;@*ȱN.8wJ\{,vʋ@C6(W<_\1o00zنSFX "g`O^ӗI2ϑa<^syf@Q^,9eD WdhyބA/zÙu!V ZP[k6Ӕ-aؙʏҕ&'nC SRs(`zq/n߻*V"_y@C/zsY(`?KED>ҜX'h6 M*Е_qĔYPd$5zE*M},sJAxMrІEw=P}gZ|Kz[m0`3JM EP;NWlO07Us"8JHqh4wzoDŽ 8"8, C:'{\WA|}%I0줕A rNWd͛<&ϷRU/hHoh5$3>x;)B kSvfTW滱[r5E[& pH'ӴepOLur"Pcu0\7u-J"Ro`C]3~P{]HZcM1~!"9X7 i`,-dU%05[hn!˦K",IFLReO9zori_&~a3 4]e+RՆo[g }prNYNPrMg*s~tx:ӂ)dw_npt/A( }6"X6.F֤Ҍ('Єk2`ި96/ڢs%%m(uy6aTKM&'l^9 uMj"bͽ؀l'5Rns$ęgmWxdmg5jN{rs!rbYt;MP!sdJkfqk[۪9TG^ơ\T$Q T$ k'v A;#`Q*ɘ"!A1HՖn`xG5ܳ&9r \cv8{IU؜ -З/Uoɲ`' I[ct@m[XÄCmlLz4FC8I0ﺪܙVTz K6t'l+^j &A D x`W6];Z G >Gԭ:p誯̈́üY~P0e>]aa,yɕ|iLĚ_g[Ӭل-^~.ifKŸ$W7$;Cq꼗*C:Y>]ZSEQȳޖl^`THp Mfd ѳ䌳XExPy0$s<:!H*B$8册Rzsi%.f&k_&+(*DP"}[??V4uBS3fEqRA"SQd[6)8#3 V઩fy7ȅ pG9)'6bs}8b^+HJ̾rl9ѳs_,y颓ykuJL-g d AmI((0h?+A {s&tLvZ@I1e%rU ܧ3QT6%񾮅,YRb,{$x֌;]im'ךk}We8mnWۦʾx=mbMwz5$+.NZYz̎4jMҐy S@wA0 4r1DN4N<죑!ue.%˚.AlZ>9[|QkZw~hc;rLuñ 8fC6M׼%Wޭ9PDgnw;]}9Pa>"H=X..*c-oD^<,zVo"P`WX &Y eݨg0qZ@.c E3NxVK7ncT&l2oGEQ/OACݿWR҅o[fШ4ZT5wo^" WNgNq`ފD }8Ls(iE(teS֐uxLukPa~'Ѫ-WJs䜈|$8U^JC(6AϗMc/kM| 2Wt%>g/b}BM6٤,[%HΛ]裗 HP V_Z!վΛg~8-^Ul5&[ [R"19Ei-6Q%ys5JGe-#4`dI{EtpdS׃ b$߹@rxKR-]s<@t>AЙmk8H65 y+:ˆ-k;|}wX qFjs X+ʓbw?Vt-[MWq(d`)E;wôf ♂ V8-yFuhoN`S\QC~qx.vaE+ekn C0f? ˸,ʟakl"vQ浘K k=HD#"Lu&WR3~Ykto>z˻7ijnEqUAφQp~)x?:+,0j(E~6(bC$,_t,r6S8 !mN(f0tsR ?=C&M_rޚ*M\B/r3"HGBh4THC|TVfl`2%e,fgDE4 *jd" %ڳxY0h̜"<" &)yb! 7jA5yf]6YnmFsmȇ 8݁BFoLz"2E֜c'#x ) *By/DWG)m#}hj3VUˀ}T5 Uϱ7%!PAM@Z;ƨq|l63ߑS"Aն!*<9.oM-S%K7ExccqimZ&buTn#t}Ҍc(Nmj1bKˤo'O7 r,YXDiyIv(撰*b#2x(@8s^>cG9ŪM))LQIp֐tUI8 ^le>ÌQC'DZ0f3~Y U DawPMut@gcwJxB,:lpՉfŠoYQStӢO1^bjea IN4iZK{Ӽ/ P3LS3l}letEϸH(Rئ5Ťf/2>TO?&]LkW &b+ze]KS4*…[ "Pn2Jޞ]ZA<<+yc>J/s`S|6{FIy(B.f-p|'ѻ%k5jwy;W4f+x!MPYi&7WOAH$ZŜ5BsEՀ 3pA.|@񧳟>:@ƥ<D- i䱧!yb^AaIAHbЈIO퍟?!\ _M39"A:P>ݒ5q "dpXgG\U~,x1wcZ=pM/[PtFCHN65qMNj0irt.ZI;,}qK pP91"wGO8C,'k[l-AL$wiME(?{9oslTglQA!΄pkHs)r)1C3F?ތC}GI+&V '#JВPXT$Z=dkȨb٣IS;EU47F& 0 @)tU3I%Y W"Hrͩ /f~FX7R;5,rJ1?c_+~3Ŏ+C-i w)B^VŞg$k\ID^Ж-@M K?;s7wX!{4fm.ְ3Ӫ%W`_c'VDDu>`ZWocnV7< WXAIyr 8Ѕoz5yW_3s-˻C'Wd`,{Z iRiːEI獝¢9ɜ5==ȵ/K2SҊ&Wj#6N 0=Pr}8 yԄOP9qL]qx]κnympRsf$: N~8v& µ龍[AXܛhp؊Sca삜wL9izX, ~Ljy.y dUy H5.͘"džkZ"Es7(YsA7.w Q-zJq%E~ch(@ 6-.^ҁ FwnE?w(I,[!x`=bM$c=j2D˰d|8&iQA:Ub#e$$9D{r^ZU1mCz/Um#UӦz./!WzȐAQFK;B2}*Ÿ/ʿ){BNp0{vK*2j~HhBUU"j[ :#?ui7+Qhݰ"W%Þ?(|n>(<M~9ZbpUEUe땯RjB|We|e&h1s( eI P[BTND;%l[]Uhد>oڈ^l6}}r?FtÒJ%岪@C"7yh Wct yX = QvQ?uTZ@8hPIU67$j>76Nx5 ֥PbvuhM ea*bP lBo).ȅB*D3%炵mS("lR6(C:=V#Д{#z'9ۋ3Nhm &ժoCΕP-¼NƮ0AUm "xd*!71bpwYci~{?OנX`:ьJUrxfTQ15Kvx  1gCz65`}?*]] 8E>#-9XUJ<w簁MpFW)MrǠN '?;/9rT!j#{~wGRsigHIVj| .,6`ڦ!E3 ;^r#4Az:/Ra}8˚*z%V\,$2;*ɭP!uNXwU5~uAfn C<˒>byօ n+_E3g2mf@M]V9 3E{$Fu}p Ȯ܃wCjH\\P!gQ$%q^0p#u:sbdj>tc03\O"{[5 _F"wdlmsCvIY\Ϝeb钪$\D~L$iUvM1`$z܍JMHY .z𐼑7 D].řʴU wrEyi\!|6S}~]j1xqK.(>T{ vuM ~ئ(+#}"?hwo| J $P#UofPDeFX(B:us K"|*aR:u۪cщT`l2\&_WZ<1 3B²쐙N?*©!D+%0H>ƮB0oC#05|EQԧE_vb&I~,0YyS +õ#~XUQ1֐ى1*R69\&i ?ʋIOVUїJ&ŇV $`?W)#KC,]6]^*|h1(Q $ݓEk s9{ٸ+@B'(sL].oqd$Y+F]>좑Jפ^3@˒k)/zgd>xyB!;f5NzuC39S. )VCe^EwQ*/o4ܒX!T Eu+ల딯tKrMXĞ ODH*w=[{wAl {7\ < Vx]t=6mU܄:yA%SDlM_ꬨ(iz#\67/Cͷ[6=PـЯ7ioFDTuI = Hl%z1`5u1d$KZ4\1ǖ":Kϗ)?LRQ!=lNz\q9T"]o,*B6dh \OU`1Ւ]r]G2zunCT@"(A5h&Ɇ0hN@]=70 ޙϓϕq94O !Pr{vCT*5놵;єV w f& ȟ5MjE<ذsYv[RBSU)oTt|h͊g#'aP:Q5Y^'N|@sO߸#dZpC֣5iيN0Ƌuzlɒ6ܞ@*#ލ뒍#I$F7Vs zĔ@/%g/wUYɚ3Q+`9e3=4Y&z)`bj; ̔,wc0u딌ި%KQ˚GE-Q.fޏ5^~>/ƽ9$#i-p*yJjǐ yo5뎜K` &urx7 >^HE_1f͘&ϘDK6 ~nXXȬܛhPrTr[/U|o\-kB /BAMT N;<3:5krA$fAh]>Q8m,hDiKBQ/ y#s-@Zh^oH@64;M6TAב|(l2ayΘyt//lY r̓,u*e5S[)!> !ϢC#D醙B*0iN~YLAb.ז"tbfCo$p1ex N+gطL@Xp5~? bs`@ڀ@;DA47$xj4er#RkB9K?lˉT_y2{i&0N1*fZ4(rȭow@4'[AY`;VSJTe\;'؇!DXh&K*ӱs޸zUuwj&θu۫q?0e|mB6ISwՀ£g`|.oH5y4dTPN@ޛOޙXdojhvC /O҆ $]S otws-YsE4ݬ|| m֗a(Aa>g-B:b""P,`R n #UygTu0riVASX6{W<5:G !syq l5Y8O9#~i҄/9Z-y~wǡjj'a,kwu5Yh?p=RR4 \Ī*Aˆr^@(\XL5)U^2abQ`f$BŻ9sq y|5즗"c[rᇳ7:F1 $3]s cmL&(}tǿigcl^x[B,:5JWMR,Ʀ҅0XbrE4`29~5V 6Zn`e!EMJDM%%(TmZxQM49ϗ(jM~"/v1] 'bBEBmH>Bƶ(EO&xBFx3NmpegE,ͧ@ jmDadU~ wXagFdbFם˵:1%xk0Ջd)Y}i{!xVq!lZROZa{\Dts pOx[B.SIl?jg,F6#OCh.J eQ\Mrnlm'wG w}y5B­)CGUkupT׃4d^V;Nƶlu|5LaQSS?jxWt ui :3_sW8١.Z/%ām@YSod t%^&Nn2r-A|Ye9{ػ#2g{B#lDx%tM]򇃓?LWg>_~#v>gv kROck? T!%o5\9zN uvAX]/TL6#I7:F?$ȌYڢ: hqcWgѸ@[qkMkbFR?í63Vw^mGd%rF_0iXS7'FQ' 0V]TS*9no^|ouQ)UNt Vh|z{ g] UGNY0$;xvq~.S?>JPe +n~Jy1]F߷%S#ϲze 6pn(=gdhYzi,ޕ۲m/@)efgb7M<{%G_m6'&}xtDā13ij:" DOQd\f׋fƖE,)W(~{~7?n?`p9Ōָqi]V>gzm[k[0r~@ *alKL:@0̹ګmoYlȥhl#t^]yAmijAxb kŀTg(u=u~-g O̫-t@gyu!]o/r.pop >]XEXL02v۽;Ӆϴ΂΃Û kϤgՋVcf o]L.vC xDgŲ}`"%.g8Tz;SEz-GwáCU5wPxvB˅9FђP7֛mgh֖ OG/LwC+t[^K {]WyY.ȝa8#'8w&sֹ5:n7E<]9jՋ嶸auIOXpt~ m"*`I_7$`m:Ta$n HZL6n;r?$+ϔT]S =!~%G0/&Vdc9p?;;e~7gr$ShOOU9MK4{M 3e0q+9ħ-VzMrʶkdiwQp>]/m>zDXTt8d}p]aO?˞k|IWQvI} ])*Bt._tT@o%dycIفig<Ⱦ?4a[G$}k9M15~x.n;)Ga誳$сT .. wQo ʧFKZo6 GqܪRuZ19,q7mъIpf ~nAQkbm5KtG 2j/0`CWm>P!~}ڂ*- oc>Yq)]$g %bz˪,KCs#(0̦$4bc:~ ø?Ɓ4v) atz-"z(r[Mκ2vYV{z][Oqc͋kE~h/B Q2$0lc鿿* p ?kIJk n> ХP*P%L9pDRgSF J) i ZykLМZ 593%"y-ߴ,ttL8VМV<*L2! h*7.xDz5hLW&(4bz!@~}_oݔʟ3iC. s޽w>}mvȞ؅\o6bg]kf\Cgi^ߗl!">Jw-|"zRtGC~wk Y~A`PӒ.eUXj~_k@N:ߧ0Z/rr7"~ :ޒ !@&w;FzMszmYlp}+1p)f+ѝu-+0PȌA3ƪ2 >- YMnJ>Xxt2w*`MEh+l3|Dz6\f:3ñ"+3Xq]M!޽a'N{RW{q:7'ϗƾ>Y#PF%&( @ڒJiHυ~S(Ɗg/7uFN\Ria"6e.#W6M'M=Svk-b[z%ZQriTlv/P!ݡIW]VD>O^"zQ'KI#Ib>Bvק-yKt1ϧ9y`7ϯ5Yɋ.Mk߾-) Se-(4O+kXld132!@x7Umt4șj?ļudtYJ4Yߤϲ.ȟx_Zs^+mYWߟ/ȼk*;8ڋp"|]HSڼC6ݓr!C4+!gEՔ $B۠T׭bx[ķS5-qI1TBm6>˔mmaٯ/"k*"Mg֧ .LcGlSSd fkf*_&lk*|5R~SpN1;/Kr Z B4<4]}[婚ڡ׾bJ;n7rlUG%t>!tm/S?`aZ _γeh]qj%~}=,U |ɼz }yUڃ[dzV%Hu#i]%TGM߄%;MHZԅ]U(L$w׷riU+şI ʨk@t7:wg׻D1pt^:(pt7M 7Zu=)l<Wd2 Zkd!HW9~/0|eW]F 4y&E.E敛`d'98teK _d  _Ns3"U'@ iڮx!:OȒIGGj\B4*DZ&MI;M4%+/4mI\n"59 LzBIwh vIEw5M| =_ ߏ]yp6 SԂl߶*ZղuZg -9bJDܑgr[g2,3}LU=܋M}~[~웮Oپ@n,2_%GmV Y%,&t,:ƜƎ=r CU6:M0Ngiʹ5qSnBX[h@[rp_.Ӊ"J( iuTmYMmr”)4E1NݻПi@_١lbxKl+7c;26tۡpxJ 6r=MKĘ;`}RciBoKn+]ۡ).٧+\w:E&w"H\۫q5mxs~٣V/srqa? E/g%JY*zDP.Q#'dr-"nf1HIXG C}LF>/$14B3mnx[V b~Q5GGܩ) _ϳ.>+ ī\Fh JXqSWw-,FghS`L0TDNЄȒ(.2>-Iu]DD=AVGڠ_^ "ehԤx6cţνt8-v½Ӛ*m2fPPdfov},7ɣK{ȉ+1,\|tqK+KDPAבLmA{h҉|λ'W$ SA3ƚ8ǥ5M}̿qmEiRĊؽ*%+{F"i>էEfb`.&tVjʙBg4!o~~`OA(׮AnKK.bo9rY"PMikm9i8mzV.s_H~V۾@ DTf e9RWEi蚙} l*u^#/Wr#(EE^WuVE]osfi.{4;:IY :nGػ]o. m鼄^Y 0֊ԆDt"VӵrYl |}Cy\XK3a:+( 4kT!'#v:m]NқV|;|ACbAw#A;z[6nJA4%w8Z4&9{2V1Ȓ4Lj0Ą;MQ"@s#oXJE9 >">Shu cJXibo2xq5}k8bn1Z >:w\8t_O}.>T(/j9gض%o답f~=ǰb'K%4*Nիh,~I;$1EXwM%qHhαuS3UFSuusmeM b'wX8((Z]oV~X(ץh k̚b͖ _Fic_ykKuO~>Oþ<6*pJzc2 h ϋl{ MƍCL7}T!1]Ǥx(}M(=l;e @&# ,>3G蚳FڬTL@ܖ*ZZ5+}Pt;5h-)͒ c'GBc-BTmbZ)<*-P@fUxY.>*72~̺R!z4o 6h9e7vE{֛]gcBi:2ğW%~|;>㧋mҿ_%+B];E!Gʌι` ƒloB!s1hA. 0̨:jfk'<]J㾷9}=͑Iۮ=;R^յ^\%m}>dn^*:nۮR2暠?b;j tXf2C[?؜i:  䵀hUozYf^۷r9C\ )8`+^šٓD/UX3:iI"a~Y9`3Zx'MsyџB6v)&QmN nhȐ\ 4a~鈂tg*16i/uY,//yPsGHjUNJxSխ1YJm"/n'.̬ŞMAmtk%]b ;i29kʭVA`k1; \xH56q\ os*5ԣ _kW_od}h'ԳPLy]%y.Ī;\|F7r۹{+t"#`S=qI5()޶ҤdеI& $YߓrN q5ǧ-/lL t+K?*LAuKQ5{WK̼h!,x?8ťEqLG~k׮9r˖c5Z)\u /+w~bTޖ,>OL)T`gͻ_[i, gkdN ڽZxs ar h 뤕U$ڶS/< cxj*܉*|JYM_fIًeDkAԟBgbIV$ cow%m%̔',0jsSE$&!&歘mL1p|??Bn_Y,sL;IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/s/000077500000000000000000000000001342332007200201255ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/s/std.verm000066400000000000000000000014701342332007200216140ustar00rootroot00000000000000VERM ; standard verm file, global engine things should be put here !?PI; ; example 1 --- Hello World ![print ^Hello world!^] ; example 2 --- simple arithmetics ![defun add [x y] [+ x y]] ![print [add 2 3]] ; example 3 --- semantic macros ![defmacro do-n-times [times body] `[progn [setq do-counter 0] [setq do-max ,times] [do [< do-counter do-max] [progn [setq do-counter [+ do-counter 1]] ,body ] ] ] ] ![do-n-times 4 [print ^tekst\n^]] ; example 4 --- conditional expression ![if [> 2 1] [print ^Wieksze^] [print ^Mniejsze^]] ; example 5 --- lambda expressions ![[lambda [x y] [if [> x y] [print ^wieksze^] [print ^mniejsze^]]] 2 3] ; example 6 --- resursion ![defun factorial [n] [if [= n 0] 1 [* n [factorial [- n 1]]] ] ] ![print [factorial 8]]vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/s/testy.erm000066400000000000000000000006051342332007200220030ustar00rootroot00000000000000ZVSE !?PI; !!VRv2777:S4; !!DO1/0/5/1&v2777<>1:P0; !?FU1; !!VRv2778:Sx16%2; !!IF&x16>3:M^Hello world number %X16! To duza liczba^; !!IF&v2778==0&x16<=3:M^Hello world number %X16! To mala parzysta liczba^; !!IF&v2778==1&x16<=3:M^Hello world number %X16! To mala nieparzysta liczba^; !?PI; !!VRz10:S^Composed hello ^; !!IF:M^%Z10%%world%%, v2777=%V2777, v2778=%V2778!^;vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/stackWindow/000077500000000000000000000000001342332007200221605ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/stackWindow/bonus-effects.png000066400000000000000000000724211342332007200254370ustar00rootroot00000000000000PNG  IHDR;P^CgAMA a pHYs X =tIME  %YhbKGDtIDATxLWɮ9wnBطY `1[Hdμy?~stU*JJj;)GnӯR˅ ՂǕ 훅屫iy)g$ڣ|.靷KcLAż3a8.Y>@,A ͍ӒIBEyJmώe6j^,O[EkB)2سR¤!*imX7Kc?f]Ubr&J>I'?;ecj[>V\C8 ۷uwqciqy4H--œ |]G/ۓWwf|ߛb]a*}5 8$Y`Y|.~ʇfm|oDm˓;s#[l\̢!E[K6:D(\_"c \R>z-;pPnH(- aعXZ?/l`G~|z;{ { ׳:>rԕ/:CņoG@`)ǥ-M-F6XȢ%ðʦ+ W xD˔=_n]'\>azT{q#9] ~?Y, |_gKd'a.*4,OLC0[7 # BVoY$ŝDb/mΗɷ;]!F;ٵXݲ<,FUqɱ)%p>t]bɚh% wEX/ y fC,7XÚ A }qZPج꛻ݡuS[(j>pѾUGy/$˙}Qd.;lc/2}P'bɛx#!zc9޹ht9bhpr؍k?- fWj 4W`;(fY HޝΝUx܏N=>!er41p }SGto{w#s<s=~k_FA(Ou0c R8,~o!~ok6ҿ۷?*U6'oۚuyMatBv7"z7|nwt+F;<-\֍(;b&ҿ/{λzٶ)* ]i/kkoo'Zm E:LGQ+8ߘAʷsĹvKcD}rZ40uXH&Jr/[hVԦ1c!wQǛ\Q+|x鞑MmMgzɾtf7|wD\H :$ @_Ã{:i:2 ??:wLA>e"6|v\y!?`;ɍ8;qZ}의q"2r|qOGEX\<w늀ZH7 Ǯؑo$zN7#sN7U;A,LE o!os P,gY<`qUN(TN9}fovd#:<8ܹꮎvF:s/ˉΕt7{o>ѵ3ѻ?7Y(&n*ץۊa*%.K?ЕudW*wQΏjih—ekAmKP~2tX{Ep 9$TA!-vRjq K,%rlb9?S)Nb2 g>;$S6d^L?O2t CGKʎ0k Sr%t 3tc: sZ9u<~sA ȟ\СE~%}8?au U>fUC+.f $r:(GY>W?SscNWr"?2uFy01_p5!u)NBwMR0AٷwYb?Zoo8@B$QM4OU߻J8? N k5"pt`|堅@Jc 7 n'N5>}Ef>7Ðs?ecCh;AMGZ8WuGxarn:hk¿wF~r^XE :7Gᨭ8Nw7):~Bp S`yTt76۬{`w ݍW_qhQI>CĘѮvZr4=g$ᗅѭօZ=ez]Z[[7?wg%AOkۦu0Bw+6%tr]4lz) 3$5v2QEd`[wN<0DgAh߃=F/ }ax0w| <2yh%&3666`.F`#kULƝgEۙ upӔ] .$e :1UxW’GbzaH& ;N2k1 GDyuC!Y_ j;Bʡ=,sƝ`j~fy؏>ʙNkc={-@ Hd~2Ѣt{ÖD1d;[Gd[&:Z 0{n?`֕i!t &zhd& } F`#3Cp.v7HG`lڝ< 0 b41i`Rp(kau?49 0~o48sOhQcLaOv H[tAЌ̄s0L@EI^dUCfcCSShsڬ4l1:S8l=~$qb!KT/ Qz@Hq+;\x'ͅGv_q _q ȓZ{r?Y/lHX{ '!|l9 _$~eT3bhPn9 oZ'x<㇁rBA_C!0o =I(bU:αy %BYObH@yC['- Bi1Uuгtr3dC(Z\xփ4OBvp1 ڠe:^HG8 mC~0 BBS[IyqWIKyB&S7\1q'v([ybh}Ӯ5_*INFnY_˯("<8JIJO`, v0sy;5΂~*Wz@#, M[sꆵgSǚ( ) +_EIpO"J``i2 d17R LIȓc<\=%a0@N*O%H)8v: ]Lb\1wHcrv AOTun~14@_f/R۞=cuL$y'azC=Q7{8uSqw[IXE0Y<I2dLJy+<)OB ͺ:Tz}i:߷ؠtSTU=m3pEz&fT<땼+:!T0ϔZz ڔp!Xr靿p dG-*AHc뵪Ccrk:P ^}%bz6JZѣUۋ1hz_7EhJdXO2W|Wg&FUzuzQc̔V`#LhTie@P;φ@R%qOMTxPaLPPuhڈnzU'Q1ؤ?LhGް֎{TlAnxAA *juְZO)o/}BXѳ(AO(Im͑xRHqjy>?9VTa1!U5›J>ݿ?G֣S2sҪ CQ| (&x ]lN#L7 wY4qM]e=GKӀd*ka1q'tR^"e2WT(yR_ʃZL%Kz ѬpVL$1z+~5*5 6`O?U |>GCnZ+_fulUpR^ x+( 5N vk=U(cr!3E\ Luػ<4Roō.%I/[y:xzfc$;W|:F=q=*G+R>ye7 z"\I_H 74[`8BŸ_JܪH=/┱l>b;祻/e=e%Yb yMЊ(;+dgK=lTBJQuftzu^1A`#j\ow P)/qC_6ž#I }Gd%fgߥ_?7KDYO^ԇEL8~iUJR/4zqzEp3E2,att<~oBN䵞Ihc(MXw!(62NZsY4P&~og ;}DGl7?WujßOO7&-=a¢Y( 0FYZNuH|!ὗA+Tu܀E%RuktU*\w_M>0o7}T/ *KD  Qfa˗%H?KzF^Ig>mǷ^9EŶ.+}?$cLdw 1TyQ,su.NmIwjjK! !$@16؎ߘ88$SdݭRQBquoݧ97GplYZ4}=lr:0@Zb λ&Y#5~!,3]fe=3 [MZNQBqh HFz&}ewq|9쫸t*^aGͣ%o} G>rURY:sUJse@?)8?LwfE PEWHrxdAԟ/gϧ۩ųYMsbE 5Z^Bڱҋdǚ/ͣ=f%i !t=9l0&:Lg 4 aA\v<[֪7]^P"v'{t|2螚e@?%oT!΀rk(|`BǙIP*@5Gђwj.}dCA1G8Xtjyj~4zij_OQXMr Pf4#[ȸt5=4Z2`4 m殱!DIk|gVf`.|%uWEzsLA|!մj!Z*)@FdSIwh{u,H#DN#n| ,3{ oqIM~d|~X rAOV\,zǨ=0g(CQqi8TS򚢎>88A)H3¿ [PeL@S9>/&4"Gg2XBRA<%t&!A$Arۥ0p)ǒH浭*ǺG6>#s=gFw[>IHT`p;,nx!zAGk\ ⊧L_km{Ҳ<"?&K:!n "Iy` V4""DS=6pA-)z&D#S$$8Ӛ?:^lxjQ㏺ם-{fɽ_fxû]r~[S ( Nu誽jջ^*+;b ΊbEVjtS44 R*d^QQ ,L>lV,* %MKpDʫn^xF{͹s}[0~tX7n]V1F1J_ %};P5U u jƉF#B\zoNgviJydko1n 1\x< ښzT1"j,2(ٱve-KUcYpX:_QiJyeؽ+wojoy̿B* e0ܯyϲ8/Ψ%2ބ7 chk>nAu^L:!aY!L]iԏ*]3ђnMs sn@ k hn[3KGM-m70&{K[2_ {B"t,I`[8 $kpgңljm:$-0 2ħZ'pGa#rwM]/< h-%;D {ڳ}00"%dtj"JRVv)ΕN@|K$5J= ]&2DX&^]F|*r= S]GqQ vP8vB,y }ue8B>hZZ?t$^4[7vvd+ eV> M[#4NmWk$$ =8st5{rKۣu݊P|pkGܻ&S@YpC%d(%_*2^4G˓61yftttA" m!m(_MCyzI>n;Ԇ!_ov#un\iw5 R4zdͤ'`it{Za=C5/ ":3AdBA{ӯ_=? &W@OOV⨩VexÓZZ-mؘRv-!RϢ]fd!\__yaiҚ$>tE>nOA-i%/=yFw]x33Cu}L4i= ~h04C E^'Y0ٓ06e /b O9%P\ÆbkL #D!ı]DYOy2aq!Cmgn͗e+>T ,uPh^šRîLeFA 0* (Qhrjq̌j-#8Ќx1y:V!MWד&Vw9\+gzD1Y/ dTeRz 6;0e9cn4A^<TM:ܴ:aA9ֽrN0!ͧrYSZL$J( ӛY#Ha [mꅪ)I8 /f"mZj!H@K8`dTiqndH6I؉ޮUYi9{C$^C:&qsZ0FxR_Q Qpy.荣Mbm Ǜӂ]Vp6 ܎gr)h;C\H-W.jݸ~Vp _t7Qܬ|q~&Sk%+^p(l o)E!$]a+x$u$#bǬ %L];Zͳ;V^q o+=tLlM{ZmJεNV|-E_sqxE4Gso$D=eR(P`I=`4V U%ğZF혆hemw !VC5z  Vo?ݟ56|PdwSojvǜ(6tfƗz>"|lYw ]jpGw7ulc`j[4pI(uuNQoiIȌxD[;Y) .,[_x^EW~ܸ(,ΝEcd?Dw?3**󅆥Ȫ(, JD NvF>LLF'0R)}?~eڟؕ:ZG vN0s>nI[Mco˲nP6 U!>=]ƲoظIX-9 ƎcVu;YqYܡ]PR{b;0Ay@[:Ъ!ݴ帩a6,:8FK[_qGsƛBzQ/BwNSQo'ԯ.K.3+b F FN.am ™ᓵiB괬[SZٌaMIEXل3P@`w 7 ɼуS*28QyYc>\?y1yyeroW+PٸbB7S.䦑SI٦KΧ&lq"YBak*l@}:7CmdIUأnhdkӓr]: S'6$!&EXm[|s<4m h ;9\ts plEWn_ov_->v%a=&%z{VbQx\wwF+e=.>fa;찂۱bY7z6[Ǧ>cFp6QܓW1s+ 4N:1R ^̷ZD3^m(g*52m>$ ;Ip/VH.4DJwɖ; K9nU4I٘[)J;i=QOJ0/;Vm)4 zsuV|9Q~=n>q%87RƬ2}1 (GB<8݆X}(:Y:Xz0ڱnqQԌ2N\vZqF! Oe ΚM.!i{d!bߋQJ?s;?X>I^>79YHp{1v db޹߷:Io  -Nv:Aȱb(ʚyD(T` ;Y ;O?j ݌ ˃ԱMg|TA=>܊_sO8*.Co͹K˓Nn{F#xYۦnC gru'3W bʨQio iםǍtZc %GS1hE$G-> -E4N8hccUSNapPH.[o}QԌ{şgtʷG&WYOQԆ;Ne@pV;(Y4^lW#]v]\:?%Ѹ56fmRk3reAv xSX:{Q$ӵ!FOtp` o L>^V֫uA?o-v$`:c-]3mrxə\ Timj& *ݳ?# v[ P_så6P79d3H-7byޠ}ud=z'YB~o^(eubЪ7AnO70fAV=TGjblE+`|h'˫Î'5ӟ,V F2veeZN4 q ?7ci(9{F\|eJ:nnhz5Ä&)Sr!m1(Y+F[컥 ^ rZ[pX =Қݝչi9]cyH2iU n!=5'lÅc>MVLqo}5C9v=U6/aڎɎ {6bKAŇӷv>Tcֶ S2&=T 5'09e?CwN5"5d<Ɵ/,=^ϥIN2s>J:rvH5@8rVž[x$k3A7[D,[Tu#?g ^f*dY,^e.&|^m\Rg؊- 0rf/vʘĵ7 P1gƗ8}>cfq/')m8ݬï^To!sъݛJ%_~Y+gaX]>ϩmE&">|CƱ-9\, KI}V`[?8;YcC!Ǟ܌lH=|p?Ru:f^1e݊t@\|>",Ux>gUA#,҄ AAM,hti &0N8J# ,ρjY3'+6MR1SQ#'k)(鎠.DžGWQ`?R[6i}F=Ky0$9F^cVR^(rr s;̽g+t %?@데"@2h:Ztɔ׍Q\5HJ*-dJdo]b=@ze@m=};6VΝj+=gS3l_r2ݾƞ.WffsdG$T?PxKN$y4A|,e6(45Cn _ێ L9g‡c3m tŽn̷qA8 7vMV1Yמr2o/f3r."wp"VSb_eXFx(4,=փչ+A4DsyWrV=d@'e'+q/<-fG!\#עrxGئ ?SϦt7|e7>=W^j%3^~r}87K?) lCoa=r t:" )w)[Щȭ^s[T;˴^ +>h+u( F03øn9۱?&8zL*;wb5l0ػ2H?Ydq9He[9`8 ;&Vݿ^T.a䤯~;Ja*8Ѭ7 Z$ D(FFuI2c`?dpE APH!WOի^ K 1x$A2<\k>v7NSznj5K&g^`̇N32s. !AOS[ju j3} aaOx}vҨR˶-&P}ZIOAE|yG `ϒW`RTzuT2Q "-}C.N'' t̰RթH$qX4Kd-Y KʧiP:(_Mג .EZ/8$Zf/~Mg"hYvf+HCj(d%6VElG ` dR*>|& Ȓ)J4;a&nzu]s"}-Z4wRNtx=e4Bvā֢ ɝYc&R ¸r M lcvFdSH5 I|2įZu@P'2y9Ks"Ovys j`QV,yՖwe=_~tu`+Ǜ9 ,!_XL{ͺI ~w2dD1iW'~6b[P~|IQ? a5$<}%=bWv΄$2܀*,SCz0 %.:zKx~+oI_j+k((@tjq|$2M ղXޕݤӗJP -|Jifi}&{-Cw(]ʽ۳RI<'? 8vNZw\/QBU 0er$āp̒MǵpgfI͕o!Qa{/e|v[qcf/nSຢ=kB;a;'Zq'ߵ|~[zRv  psY1fU(.<ſDnCm.NAfQt1o8*/ǎt 0p_oLYYhT,(UfkԞxԐ)Bw5%I}ˁ?-K8]wSXZPʞv[Y% fIFX%ql}(1=5œf KkgRq{Iȅˡ*^ZI< TB !eCD#0b3,a!hua݋ R6r%c,M>.=?-+Q>cEzevt=dDw2Dgܝjoپ("$mRC񺌁KsaA;p$ҏ\H|[VIRYC=V%\Emzz ^G9O9^'f=(gc[7- |Ss0PrqhSi;6[jϽd Jd:v7'N8վWyXma[\+fXn;i}ē(&#*N{R FBhPa<9Rݰ Mq(qIg6>h)j 0MQL6BZuB` r F@pLFR!gGf[m]' ( @)^Nmi}(y8m%>:pIFc0۞W׶ݨw'?  ݙlç9OeyiF={Vq/M H+cd՝~Z =K|4M z^Q0=XY!SaJ6eET|u 2;VxdQ$%꿸,drNoVSNf;1%1Dsm6HU[p-&4%76uH-\bz%|O z0xɟ7.W8ӎDWΡM}- Xt^v-tHc]gӱ /.QhN׿z YzwØ^* \{|xۦP̹鶄,GFD#x}@\ Xh\ιۆz3Wc+8%%OĹA: itextoHĘK_(3"zLġQLn6za{3$JyyA~XYwu}[gN;?{.g%M}lͼOʽS"5BrӎlO=֝4IN,8Z֓2)5X0gwӟ& ^^Ge"HrIz2;K acUUp. HjRRgZ1$./46KIA*jTwEC4BJ^,9 K%Bk)< kKNοK\淩* h_-Ĩqi0j+w.HFEDi&n`xb{kY8/7;5ˇUe7܊psHs1Wlu}?j[#+$'*>?r{q~ۮsVH%Ft QIlT\;I6HI_h5v4P˝#)i!T#:džܙD^Oo_t,,}=E`_.Ypc !å$u 0[Iك8cMgNY-,G[x{Ɲ||CfS eqEeQ߆Iz]cpߎӿY36hz=픫k.7rЩP ݔlU +mTfMI^B$;7va9K Jאj5$lpK[r+kcqZ5< ΍%S| `Ard T*sh2rI)4/i\ojF h(ѽb | t3ai^aôgq*äpCl u&2Ii <c>ƚՠU Rvgl7__ IܙhtJE-({lֈN ǚ iHt]xTzߝ{Gb9=QBc[) ̿z9eKQ\kTa>? /E{4` 'bYm#p,1TiN?pT0 I7iXÄKC?U1\I(jsb)$[k%2+\ -}3U^=/vѱ#;Ig`4@OSqqBc];vYBK{9:-P"oE!b,jRm}2@NN,ԘkIpY)eEb#S{;|+G/^ %L?v|rB٨2tf/ nMiύ]ǡǼ(O_b'fiMo;-aNj1j~6NnӶ+ e ǯ}Y{FF.ꅱ'P=x!Q m {FFvQ7 6%L+ITnܩ6Dd`[{/Tux. q>6jRhldx,r=F( `{y W |\nha $mwi58rɝF3/)t_y[FA}؞OW`qr=@$v-㙠8td5!wtXHMZ" TE \HDiQo?ޔgh=UA`5H9&Z sFr8=2Syq, 9c˟ZǚrI\4w0Cn{B]Zl4 w6|97+M ?^ 7S]&OqU%I# ~ Q&4:s.0qR۾kH@d z},kkazJy ""[th5xt+-}?޳Z﻽/B}1e]Ug|F?gӁlG@tz\;Fԃt? 7kw6hd*'N_/`)%S_?9j!EQ%P(a?;)p9u哆qɬ]C+ cxAݺGr@{̕LlV.T`ǧzUPÀOn $6t k5 0xn^'Yw|=#`9ζGvR8m4;{w+;@~MDl:hFI3^ZϺ +NõnQɡc'7 VўV6,VJk[ZQ]\xB~Y zBlt8j+H!а9*xM 4cj)n9 TQ;l&u,fS`cy^4AyclW dҾՉ{=:]~o>X`-G+\,2P3=v'bg31<YR3(t2_< ] 5-aƛ;ôZP^d-؇ȑؽv& MBk-4>-d=Ym%4J W+E%8J^ Reg=.c5OSQPV[|5oˇGw\X(X~޸gh"H|C:種drG0BHB#h4 BBHBcۻg}ק߽VMMM9O/ A1 &+-(15{vcc$ď o\+v|5%hE=Kba, laFb+ooI;}]hI%gAզnȽP!jњ󤬧 r?I؇R|<8~,cȾ2 .dF!b ]|4 YYueZF{ a{/LrɻIxŦE.7(G~uG%8`Lx aP~+~ùOHXJiAJ٩q<`nSHl2%X$PReMC%R6'zߊUMW 8u=S8ɖX+2 *xweU쉾bt5D'v#o8|#(Z:f6 "$iVfR CSdzBpPaϖܾ)mCˏayݱJ慊UPsl𠺓|,ex +u½arbi5(L UAv pXHNgs5YN މj#\.{Fd r"8JҽS3I z5-8Uk1 jt9!U&Rms~c̵P$n :#޳epaG2[M_>{/^l,Ym{mpy<؉"QFGhn%[4&xM -F{P>W4^dzc>}DYG5SP@q4"(8|ˉ2rKnԉG}B^]E3\AÕRKAj$h®ai̻,0jZK#H* Zw/.kΥR1}=e)PN?aYD|e:H)1G짧!x)[k!1)h-^0 ~rdkz_[][,LD]%}q7;wvF}:"6z E1/xtxѸSW~_'ͱ}0^igRHzD0?x6fْ&W"I}MWl0)R HW7c:5채keR(N~_uA J_{Kg?G"#%KuxhājnMPRKddG%٫ =zDz- BIPazo&9D{Cu')Bm^hP8S*PJf*_è5 NZjoy{F'ˁNJeRwtU c'Eq !{?:$L6+}jH[PW !J<J1)E+v7RUcN~3X@!D.OBk) RN 4:jp:{a?9[+kN:gr> xzj;{$\(?)om36!{݋F]'5}DJ"da>Q윴wHudBx,)@R+`R5<}WȓU&cU!`R8g E*YVnAN:I#z |9P;u"$7gGI6JF ɏ=@4wz@~?${οqFUvovۢO[7jtv=<1` B ˣ"PX.p) c~f;o~2U39 x@dv+H[ -@[Pe0pC:oYk^G3yq̔ucr-I!/p EXTKCQIEn%a? q:q׬,+1pAHڰsjܧXv"| 9Ɯׄ"Ck^wLj/^P4gY{.<\ +/0d{99%2#9%̍^2ScSyP8 Ic$xl]i\߹詝NG{I]8Xnܚ'Rf5|Bv>augw2c1\JGOsd|l'Ld0{j{ʔe2uRC&Ē /CgBfd>Y?"fP1FL~2s߸=R>􊣛qIi-ϙIEx$t;UTN"yJ%\yB7¯5o"lth ܖwH.u8Bࡩ󎂡:Pzlo;5YMWQn`*w*.=z%,]S%J K#tx2`"^D1QQЃ&!3\>::$~xp9OFix"AGS+~Ru.bl+΋%CISM p!͹/i6mgOt-5EPH2Ϛ)WΤ _diHJ;o15; .?Fq,v))l;Qvۖnpw2=@y^xD-FCJ fpKaK埑 =aۅ^jZ#ý?,QN3>xX2e4GrOCbM Or۹2#aL*~5Yk=LC $1{QDUO'Yre rFۭ׫AX7B 2|0pVr?[wP z@//MҞ(R^x(HRA!y+l\-@7_,^:XK, ~D: CmrfsQ΂I%2L#h>":淣:^)Zʯ OFϾk!W/Ud5BʈR/7AɅnhp&LRfJ=4_:O}'Q&&sK,n?e 0! -DMәpɦak221ڎ|;쿁mW!LcwuJH2| fN a[ERC7Njduȥ= 0}7 U7H^=g} %QsN_Xѭ#TQuU^P.1>69\k6ePsT9f` 5 |/ u|2Hv4lœ AL=xﻸ`&ayAfepAǣ=s:M=zURorx~9D-;c^z<"GN|GswSf'7(`5px2G1ɥ_\0( P,`\dq힇T(OcK<%NoN-v4 V6@{l:̝OKՠRVkOTk"zDI [$|lp!BElLLI%tz=R/6{%T}Ja;Ju)r+xȆ|QntG;{C{+) ף ׻PQ#*0y&ɴBSJeK7^ G2ΒGp*R`P wGasst2n" aMrOے:S;7̉)fS4Ux^IU=^ĉMk)_NJKPŢVXSDz"kIm$n-wivZhd' (&zL?r4_&v/ZȳR\f~w *K0ʮ׿]ˏ>y.)ޮ[9+O*ޞƒ up/z;ZԃJL,4\/ɫ YA1&(,'+EO2h:YxHXZ3 3bH$B %HNCKNF& ҹ?a7Re.Lqbp1${Sj 4C&hl<9-z BQ}ȂbᢐҜVTΖ,N@yF<<DRXp1aR#%m#̆ƫno%>!~SRѻ?} 42&-%BKU5_ 6f̔U^S!E,/_,i'/]5{LK0r{+Jsa-dhs숒K YjuesF$2-=۴0gK\7 J4a$'dB`,P}.u^ [tSTQ&-f[o n^ߜ- KKг)9Xpf!@&悕YRƜ}6XH۹ԳW_B$SdK2<6 Υ8||9 gJt@$0E+2**|
$l%NDM t tcp+Glw{j{SS=a!gg&I.nz.IES;Ala7.׌>m~;{v7"*y'b8c߮opcv!ֻ' d$&2V=Xgc2\m$`i^fgؐ7=V:ͧ[+Ճs*=zտ7j5i!Htj|$@!M`.JS>п콒" AY |C{kO6? WCp lSEOvԵGN@!9FH|)*9wb P&/zRHHLVGf">ܵwͻvr>KϷl;?~nq70k7~>G6ޝݶݰ e>~δ#î#WLI``IH`BR]e$*;ןﻻ A5;ۻ 3Em/)<7|~lQ/ .N xp,:xߋh6TBNSq_1 ~5û GO'*-?)DY5Iq2}Aow ߃~ G֒Ke]T@w*>Twv*qNvzv9p⓼dd Kn?FT#!zⲇ#=ܩlv6*E27:=^Rȇ~<&ܽH_=O }>yHFIثt=RNCPlzBFE@E|m}ϋ"݇*bH Ic+}Eמr۝jnmv}肯BA]ke_Y1C/F 1K~ WhZ>2 vy:&A~x=:My (@_HÖQ@+;QK BŰp-gScnlxYpMnrvNJϹQWs6tmbY$_2Nz^/{,iNHi泲|%Yg~ѓ͖^"K}ʮ@$sm0H{xV>v1؆*rMf{]&t8%gJMG P,}~iJlŸqzc"גNkXHׇȻY5S &msqBP= 4^XKZs|l?};Q O|׮2즊, t0xYYL썾~ ~vlXsg d?c1IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/stackWindow/button-panel.png000066400000000000000000000742761342332007200253160ustar00rootroot00000000000000PNG  IHDR+:gAMA a oFFse pHYs X =tIME  }ȟvbKGDx&IDATxY4˖ɇ=|Tdn"HO zjHl"t7sw_ɧDnaf?yCk7&\2m S3F%SZ iSB_nCk`4=z4w@9eW (" :İ0 ; ,aJp8g.BY0OBҖBC})"#ԣ a~m 3BbjpltPݚîn&D)1sć݄~0fP'!$h#:wD8dlj#Z O!Fu+-1y:ه"mc)Q-.͆%ߺ"   Pu"$OL,! {82"xdZ {a@_~Q-1n GGDksȂ`Z@桬j8nnC |F ӹ tu) U>]%t>,`[,ͅ*1ynkn%!!"4"BDIt}S?UVaYP j Cc.Gz #`bYlhˆ@D&Lr?>&4+LP yME3ߺA0ޚ!֌!4BDHQqhDޏ`&|ϳ|OۦpSA{fZ2"޺Fe,~I}>[ z! bD\eax9u!ݭ[BD0LpI=2M`E6GTfp) î-D)3~c0xmF7D8e )˦?]!!Vfԅ&4" 0/Sz&چGF>i}xS#a"M}NC= y"!4^zI|̄0qx Bb*t_`tupMX, w!D0}8"܏`p y0bN4ԅ1!v@+īzfZd!؇ 0f&!5<1׼(]-}U@nwupmv$^y̙Lxif !6,d~}:d~t܆ 3r@S/Bfm0 ,X#Axlc!A̅x;bf5}aPNcA zbLLτT% duԔ(όk]=G}?S ~>"fut(LMP#B-"3Gx^M"#=>:!L2@Fx!"p fao9V%I |s ė;<931s\pjڻ1C߶~.3eM"=$fu:E {x. >]m?jp*m%t>gIMNdU1ጸzS[A,|5DdDω,-Ebx ?]󒑃Bmod B7(Ąڑq?)uU#a)4 3ŷm,Y0iL7嗵9b%9mO F7@À0ٛRbػΙJDצ?_ơ3=//돏jtoex"#~u@aHL@DDH ^70&ծ=TR4%sA끳Hj3o=<1N|l kX0d}[;TaCe$yct6PUcCK7I%<$*nH|Fv | 8 x 08@ܺMo>oF!DPau[ßxJ?_uoMO2j"7g&w&5/۰Xצ-BA6-HFs3 ,0mLY$ Lߚ9~wJ/qn#jp݆D]L {R d6ܻ!y}$&2HSHLLEĈJ""đ@Zk)?8J@4 *`L5 P3]E)8̅yb2', 0y5{>d0@\w/s1`v€5'7 |S ( pʄLu9Љe׿7ํKIb$S_O >5 ߜ߆{@$!jz5։ gt82n!M?I8kSf(0 v!U#S&a]"lט3BWFM?r&JB bD ! SN|kP0}^G Rib|2jj0Cnj領9>4"<",}dªȂ[T ѺFfDD3s3bhj,ݣ`G$wuu s_ia43 TaWE>\ P;xF>z(50>2~H!1fOKpػ{L "1"wO%4vvwDGhG@"픰y͗"3FbRG8=-iηK6̝u q[o,mYH=$ps;tI-N2s$&Dk|utC"jHEi\e9SNё3N }3Lu3 ơ>400 "JDML! ̙_C DuH̗ݿ6 ?S$pk aR*/k3}Z F[1,_6-[O`fΎL!2*w" uC`jp5 KH! * ݁ XBՅ("]tDTzM\!r.\ĩ>,3L@ڿ;}C! ޶ǰN ]į?EW8^Ԟk:.kCa؆;o͂$u [6 bY01"G6u/If 0B6$dj!L`H5$r L]Kb$ LˆA#ҥk8&Ϋ&ƒ) Ϋ͙yuF uC[T{ L4 l@wG v| cp D CLL#:!~r0v9iKD*X4&2 sdm8UBMafJ@t({^& cx|x);̙E" )є)!! UKu3 \26 ͙FNZX>LBuxn׎,tov*<C dqhܿ?f C\L" wch +@kw!jj8;#vKPМ4]=mmCfBa8t!dnʈB ޡw-BOzN& #̅Mq;O8z>+Bw506 !;z(L~W ԣ0m,2}"D5S7\]#Z|]㐙 ] ᐆǝ@YpXdƵ3E9:[Oe{ !zۑe)>dٺ}}fjLT?_ovWc]}X`BY8ԃPٝԡ 0"9FX$nS!3]Yx;L@@_n* ໙la_?ǹ2QTn}!~|έ7*viЦ0cs,4A0i[eBAHITuor۪ߟONSCI:-riJu9^6B8EADfG:T +y0(mn(Y=)Ua&itӉ&J_}H=ޝʥyMxl.w7 Y sJcNo`)n›?~'qXt #{ĈrMOrGm8!$Fg) I(^wM= a@ ᰸Tm,Ýܽ։ >z4ss EA\3*?Vnw;6:<!̈(fX ]};CԊG?wQ2cLHЇ-$K]~_]W"wW٩03mdShcJ[;JKk$ ~Y?j⇉_n09chLWZ k6ws7ODKX.ėVaX("^^{fqeڑRrm|XS}HSu L%{WN500Idt}{_:ݝ0=,rT\Ӕm=4\sZݻSplnSq|)6aS{&SnywfY)bUp 'ZvCBo sYԌ`o )Z||zm9r9"4 ?]K0b y0!tvSOIޙwtpJm㩜wL_qy՚&@xFnؚ C8%Ra 6[Y@$ ৛%D5˹%u}cj t1C n* SGƷ-$B4M3b$yJP("뵃y=*˦u[uy[`@uS\˵uYרZ݀)"\FCOWy)pɴ\vĪส/#,0ԜV0~ty2[Cf/`ﰫw@YQzwS;x"ݷϛ[X;$e _wtk#3m('lΉabh6)eW$j"f"<>]tr~DS‡mUxt"p)ȟ$[%3M 'jzߙ6NUN%fY(*CVa%XsMkzKapBFM$ ~\nd1+%_t}fq.[1'l%$iUp@C0I ]31ڈk"  qwN$ H4j] 0R4CaX)^AjLp3{20b,MPq뱫Æű5Uզ*14mC\L8%rqndZma@1 zZx(-ջT ΕLl#j@Z U"D+\1l33DqnՂ="n[_2J>vk׵O[sA҈Z?]G*Y00WDf2 19rxʟ/Z =%ڕ"| ݎ 6,~M0!:|&b&Ah晑D8ax̌03 7 Fi @T Qʹ U6 xtSxW% `|WS@Q7Gc&@|_fciy1sצ{oEuS z.-#2gI䦑g a&rZfzPit@@pc"{fpР%ITu#Ǯc4b574Lc s)}x*"!h BX h6|J4oÏpIPGD <$BBfE`mއ1ӜqnXydI2Ry.6t(8$tiKbIhu$+?a4˪8"E͉f"S8m.y5cB3[h @W /z >,s6cͬ _N3#:<CSd5LjNt횙.C ۪zԅuWB$B,DN z4mK݆3gKӧOYƦa`ч?O x3}3졊#V)S14 3jj}\fںKE;MlûMB' ļ+LnGr:LRx>{y6 Pu,"nCaD t߳=BX֭ ߽*9\ַጘ aN<4J{apz},j1{q׫$ЭkS ښk@f̟e:5bXQ S@}D0y̼$۰J@H{Q=^W&5ˉ=#ah0BSg , Uo^$]la1'I4 LttJ[?lGN7\w\#uDe,Y@CpDϗ b^ uu]զ´U>0?HPlq 3J!bBzġHba[)s }[ǽ(^O<<(B"P/TD~ FĩuȱnCF AWnQ# g"z(_nnpE;dGkW2&A6SSsS$ZאHw%m:3n:2${@̉XC˦$j)nw6IիB} ce1"~ ljK\(DPnß,{0cIXi6%̔ =B"<na6b^^l?}sSf\8mCNn׮5s7qb2Dv'AxӁYBbδW7 ,Ȃs" lÖ4l(˜3P73#گ"T2L0fLķJ嚘 8ԘڍLq +S浫Cf9sUE u*X4Ꮁ/f;%"bsB׮1=r[B: ]K"aDE8%,,8? !nmة0c4|m0'8QzYfBF,+E ֽ# sHt!P֔9@sbч'^8ȮPKn ᶻN9lcbZmn#pMaa`L@ϋL(tBՀ UAw27G r<$ߚ=O MBD[w y%D29ͳ##; 3Btcf Flct&[!IkAF П^TڔwB>AM0@pFI`ܻ!k7sp2*'idc"`,k=q1 = u݇YjIJ> %!޺}I? 5ߚ"saF9ft )aN  ]-&4%@8v"6еi6*1":` SxДIై0sfM%#Lk1B ZH{ht]>6m꙱&mS]!3Qj,0%@|ÔzODD$tiCaCg¹0Lͼ0KOEZp_20 ~CXcwB),0T7o_V[nS"i}7ss"!]C>]ܜ 3"IA͑ %6w4}ؒ7%Qa z~$Yqma}MBFڔmP) B_nV 4X=qa tItK8ejVvy"p}~݆;dF Aޚ_-jjLSƇHDDc!ޚ S^g 8b82b%LOU^AMB܆=(›jJr8 :\#<( ܿ?Hbb! @sM6V]͢_* ErzD{\Sa#@D͘P,KTīASح ;uX8iIPġg$`{(6ĭ{INJLdqU(a·͞yiuUmL D3M9ymĜ-|]&6<*}iFF\uc wETsB!f ^v?UvU)BסU$ /˜HN%T3Ds"q6;;D^@j!ڻ%n`[ᑉXS6 CF!' ܺ>dILAϷ^M_[?MRjs0M=?]uIHBVfJUnqW˂,5?~ٝ<Л2(% !ܫ" S#­$qwކ  ڴZBI/n7E"?xDWSGA4wr ӒTRxd5n>Nb1eo~meؾz&BƮiSSZ0p!L k{Z01ДD2mYj610-`N<3w޵1KCu{l#N9VA8oΉ<'}~T|ƇIZ,}-`uwA+a9WԄ6t^3|ӇcRX7DqLxnf!#Gp|r2ܚQD`xdxoEann/p 5@m@8Zĩf&@O Do@b|ݬ p`W9qػwR=@Y'9G45\J }@BX*@݆uDaH`9]vu+_G75a‡L*eo1 2ŷ[ _7T4%~I?F@mXܼ phk21#e=_O3ÜyP24+"SL TyX$[eyM-,^623x`3o0%9Mk)5 Y{M2gJeB_WTfOLzFpu$®0̏%{JPķq(q8fa1U>dQns—;+"RBM߮[-|.c"jES"6}#wk;^6C9'Ҭuwsvէ}mfچ+1o[fAa( B=^:g,!mU[:R0s4$= M_7;L0o挻0dXiqz!MZP`=JbsXnn"dL@é@xg)ip~۵?.#pa9ɱ^”Wp8U3Av`PݺL0J070"c4?nȑ]WwB /L|m4`) tBx r3u/D_{tivE$Ku"o-faDF!s93fP"8S]EpkM/?,~W^s½Z,N<]'SCzA/Ô4WBKY6FM* > >B!/BpP$r1$93#VnݦF0`8 <K*vxwBfySa5=eXIԞEw0w]"U0D@jDͿsfBs^CfBY=0;^q6HS}yo1%TTE-a.DFqOd,g{U!88ч%%ġ23'$ 0X( !MWK烨ᐖ]KEDA,Œ.@W;f\=mط.OKN%Qvmk9ԧqu]{M0{SH]?K›dPmfU`D}xb|(3KD%֢[\LsNu(MfpwʧC w U afKN6 )3ޚw: 7Uwv~̻[dnwOeD<˜e¸vD" S&u_j1j%1˹)9,qV4 k?^/Q]b*I:n[[&v" `()>l>U"D\5y/=Mr>̓ypxEGncf>`H Ȁ~o7Kn`pBD$FU8d-V33X2_Gܺ4 @3Ooç|nϳû8 :Ò᫁s[׭CdMc^2!n)'. )3!wK y8ZPRܴHzp"1 r}DI30횅6_c 3{$k۪; ͍\Ϸ"EL aNsLuеy}k@b[ KMQp7wO/}\ ]vws˹3mNt n֩P,~[ RK\ׂDT3^w %8֌9}C{|S1"͂u{*L u)Y92ϿbϿ0u"qN#4'dH۔XAysh  )1L)Rҽgca&wieW5-徜y9'FG$ dDq=̑0Rf vLSfY1HXLcfKÍ鐏Sb$ecp?Mڣ01ca quKkחMXSDSBB5ncX*ϕU ߚ͙>JTԚu6YYTR{be9]6m bb:$ѰRȄLT^1 6}ג>Ʃ|J)R8M F<*B%uEV—0h ڽc$> 9%~d_#  uC H̸\T֬X9IfhhA%A0S{x~b\d$A'1,,[淈[Yncq8۸ _gU f&frLR?ӟ^[*ǚNSVp5R 8%GC&5w6#aNZgTmI5vFL0 w3|xݴfFvP0zӵ3=ִ6} O"[3D2C!4uF"ԇY=L,!aJ_uB`ƒ<98È8eЀ{Dx|i$^ LAڦ,6՚Mc_xm!a n]#41m r]B26Sӧ挅q$# 7i*)dD8O%]eQ{`ݧIxԅ"ݿ탙+Vy_/՟*߆:칊1#ڈAp `SNy+ ĂtyMȌf!H/{7@bk8[>80 pYԣ[p˝D*aŎ`V3#'%`c7gp} 2x8#j,܇]!k sH /W0na͞bEPUyրc "  Bgua~kڰ|b K9u)q$59ab3ӵ۩Sxk]E]" 6 viP0 kxxavvaTB8LD83B9qɗm[ a*ͦSJ:1px>p|7K.,/8r"&NlsNnFJ<4=a3xbb&>'ٺXn` %?6'#}t*utaJF!Z2 !`eCIqwc>NuL̯g L=D@ՋMDc[?ݚ;Oy3@ȄI< 0Oo ^OU ·SBebBxٵ 0"tSI<1!$_[Fcap?={KV}8B]<.ib:di!Xݚ!Ym#B %KAD,ʷr|nT!Ӏ4[2$y5&rFN"dڻB~N@a$&n XSSwKڈxru UUDp(=`q Z)*= Ry|H -,HӭyB+?TeLH ̒qu-YL5-U"^8dno},8ɵCyBP@:|۪RƈΈ_xsO%n?_<ko׶ϒrϛ༩u!}߮="=_zuVem" v{>z7bpx?Cz*K 4ĒhtĹǹ[CM|L6уn}ۺُ!YDM)536Ӕߺ6S#`23aJ51W"" )"|s*}L˯:o[43N=GW'M:$OÖI&эO_v}tJilQ#>n&u|[_ovX’4g5LjCM53in]̻>|NRD[@pJ>!acz˦v6!fvDX|Vr+1Km $60%G(LXӁ_V[R28MtqN?Ra9%7LYRnN?]LJl_6[\u)pH3ۮXݓ\wKLMOcc!!gfL$"}D= QA_KĤ5#5QdNdG<3 aZO|WvĒq8!KC)=0|Ys1|ɸH}u KEy?Vr{&B1`T`$a`D= ?F!B_܃P-%IeTSI$ 5)3><%ں Crp,ƒ2fBD d["q(Ls_־Y$_)wgw(lLD<̫UݡE0j!1`lТqdfJ ken 9YbNzctiݎF7g9PaݰU{\aX-Fp@"4B@VM E0Ü0yIxSY싨T`WH;@Aj+:tK!!A Y/bΌ0'd%ò !1aía"{go_4MA-⫻7CUs6\$-Ou?-}HhtƜKsP?7_Qf2]%Ed歃y,}Ux~f& OvAVïן1gAk,7eͬdLęi͂"N s?T~;.aN6uB_gwc9EA@Y@di$-l GIצ月 ,y՛ /]Squ|n uK4$isWb12 3"OK91!% /o7;Djo5u."$\ 0 k?{3o 0Ð>wךE2Df*LD,)8#<IHz?!KU <~/F4b.z&NHJ`.2lHiG0saD}\0wfG:Ҧ"v(ib7sٺބ|.}m)T):ϛl=# WU Ls w :7O;fa,OD9 @BH3D N(0-nEnB{=s @ɬ@H# d4% yȈn^jӔsFcpY $F x4CSUᐶNOHOn/K7)ѦuծMs̐̇1_x_$_/ :#uai=,׭ ̌P =oZCIdsfbd{-x{74׫j MEe1&]a 3<7L!#(5RiK0nx;槭ef aUiۏI#ve^`v[2R6Bh8"Dr(͂! \0]%S2Ƶbp~Y-%A W `Y$]:vi뛹#39ˆ[[ΉN/~^z>\7 +qLS1ѐ_jYڃ&tR LI[˯v8);1wOۜ% Dx ~]p\-^㾤nqZtFMݢy̷ 1=]$^~셫ś] i^qHb fcCpxܧjQ E^KK9<-= &K>& K`9Մ C5\Ԙ^ -Xq.qc_dݽzts@|ܕLP Ԯ~D24Ipi^X8 PV~1"@8Ȃ/>ݗWvU;@p,t?DƔ `%Nrf1%| Att [#2E K5{ffcK7CB":ôh|! _V0&4LA-S&fGUmH!`(Gei=a,DF${Ijtϛ/ Ov7KHH7OBts8&\K)nnOGPfn_~1 @3 "^7O @fA(0$b?_ZD^fA/W{Kgu5" fYX  NkB@:r4ef 65`s3< q1"q>0i.=1~6Kye}KB̌ 9c3' u ntG}Ӫc⦦U1 }sl)BA/=GMOrUm޺^[d$tS6ah"%b"fH, EpDXI΀ Xbv'b@avڼE`FxYa'xV[48of/ 1b#Fxbh3t1@W\>$: Ywk; !1!<#DŽ4pfi5};ͪ[Fɜz! ڹd7 >${$uJ4VEsws w cW7 nTKL݁Dŀ|drHH;6u[ צ)q̀ A0 ,P݊ 3j |C.7!Iؚ13SJ'vm> Yn<2f~ iOY@v=];"k1?_}ϙ_4:v~LԁES8% @$eS˚uRswtZf׵?RS@bxmN[G MADR~r[$TТv? `*joVm?&ZFb$`UVc wc g3LHN v>qiqܥŐx"ڔiWxUWp*V+cԡ$>YF#K󻜀/RV!LoFY{<Œ8aN(<A#oڌ0aI\21R|wp9Ln(IjC9p1xZ;O|V3MRSqҹHZoVrZ9ϙΛ7ֿ˷~"DQ!PaE06#~avk QaXrz3( ߏ P$ݍljAfc yLxi=iO#Td Ӕx,R=M2Bc$)!!p  c(!'^gB$TD. w r81PқV @pwfCȃ_>04[B |g!2kġ[=fis"AʵiH;a"fP;AWGPØ UCk[D3Us)kt]$ Kfϣt D w!ncZG@.(: 0kXU"nowRCA0G%BPFן99ҁp?k㘱;RDpdczek.~HjUoݵ;2v7`%Uĸ8m:eREA݄i?ksA\EZwH4.D>f YTxwf~r.U`5x7 (3Duџ1^6SvO>eLp_E_l7Cx~a!,DhHm*f8>&k(,[x"$f>'!ݯ CJ Sڙ"Х@tiqL?WBCƥ:(~j1tC*~JYB »BaJ_W2mO %ѢAHc?H5%3Jpasyp BabEuuh7ȔHc 9ꡑ]Ԫ <ab8N 03P F#P"$1n9ӜdUg B ֪% HHc&NxSR‰ @qR é!ħF03SBfs>u$A4.焈80 7]1=.=23]͢`N7~N 6À^7,.]  !oUi1zL_ !v-0Kq0 /vAJu,}"d`9ڧ$k!asv6 bHfH`Ǚv0@ T5BqK뢷n[ b[-1(_{oݑ0vB(B?,Lෑ9M̧pU 9 nnoo$; 0Qbǿ-g!57u-ܙ 0=n7u!v)CCrpk=p͔<P:M?&bBD¨V 4غǿ)Doo"߮eBuyVF[b1 m7NPyȦϜ8C<]ُE$ gN@ͅ#ƁA]ȇs;õ ff7GD?fۃna %X[0 ԛ­@#6sT)^j FC +2$D&481,4UGI@D8໵LH-~?p&<[aHDrd ,F{.æ>ik{v"q@Cb0n, G.ٟ{˿߾}͂s *5{{.5#ΐqH W+I4D2ÝB]F"ѻ9#̉K/d 0/k0˵*k%$D@v,iQpG 8UZnD80C3+?_ #VOLMD.]vkyrsL"la~Tv›bJiax6>/ktH|fMm%yl^V4+jAh,DHK}Lt&YIF[ĔHp$XJNC,4 wFS!]tL!,ͩτ yU:b3۔Pz7Aݠ&y>?acda\ MRowf(?PtH$H GKsD@]:oVym"BB4baۉ.˦[ؾ,nq7}![]0BI;QTž\Ka~i7sjӀ@Tsx`}o/gr %sk9Sj8So`pK1#a&Im;,1NYeb­#V5$7CNuws Djod0!gDWu p/DʌUMAfY0us&Z) "P j!/k8I{ ,M}?1licBDi|ic/5 fqfSIna)K1e}8b`)I=|1f7s{{HOkm x᮱@f~Yt LaHcAɒ6;ws Ǘ&9JTwOu? v * %yPfne%3Kpxxr! RaqsIPx\isuυ?[~La.fZ\zd!ˢ"@nHBn1O5&E!1§AYxQ1cNܮjs}#BN*""nBͱwՂ6GH@D)ܨE(]U78Hؚ=w2w1M8 0 ͫ = Z 9_]_'wAcqIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/stackWindow/commander-abilities.png000066400000000000000000000616071342332007200266100ustar00rootroot00000000000000PNG  IHDR;28 pHYs X =tIME ]QtEXtCommentCreated with GIMPWbKGDbIDATx^N+rƤ}(+neT~SͲպeӪq RH'yfYڂdToG(mUf,](؛)C@pfm%=_VS.O9GCt /{YM9wI4F!'GT tZҜ#6ibd$cWuY N(ԗ蛇7kS/Mg_w/^['[?Xr/x0dy뇝1fE}2Uwq>*'/?̐f/mN74z~oB _f Dx7w?@p.)DuD>oLw껝f]۪O:ik)ʒ`гz:8.B*3lɋqٞ*$yR rnk6U]88qS?='ٙmL4h Uayq0Y():Q f`T*&6N8 QQ;s 1@ы;5ܙ`^/\hKI!N$KZ+|Ryƚ* HH$sMk~e MV͕dZLSpU6v$efPEZݥ:+g Lۻ'!VO`ƹpp. }L7>x1 '>9b̛LIb_&U9_n,6=}w\0#lևu5uowg>4ȝtݺ7^pg9Y뛪 (TAܩ3r`SM@G/\:YmbOTx7k_%3dT 3~@nodpsx/x$l#YER,$ apJȳh"&epoGb/w։_x` d#~ϴy"JqAlm3npU`J ֠w9V,V8: e{( bJ bT̝uF`ڋ=y@ eO[xU`-n5L{w{N"YA aF3߂xO 5D7'feeQu",yBeu{P} fq}μ f5$4)F4#A6}E7v|8#R}|BSȳhB*\D8`(c#3G:>$U{ =sojbr‘1y’x1] ΫyDS㧏u@ J&{K++x|xW :}(}Cz]1woI°V^UtK˓#c t ^nt*X$vRN/xHF&7%1NBFVw`~+kaI)iϠQ?>BPxdHV3 ӱva>hxmFә⻳cA7؃<QdG8B?@V}Ad\vTׅ; .Iݱ!ňdER|uzFHH-2IЭ )Gb()KBg'wnnnd=Y8(dsTrbr4,MPaxj,aA+ R%_>D9S١A[~%3BB*3E'穓@VC|'tVV%d(+,c*%h̜"c{t z]؞#iU, { \%1VW=re^KFjǪ< %'۳B2wln*}ND,^)(g-1Ę܊>Ťv`"7ƚ42e%{O u)b2+ ي~|R$CL& A +4Cx\hO#M%$"&(B[J,FhHi5H8 Z?4j|bd"Add'M>68^"=2("~=iC0&Int~\Ibq3weJqS<`OiJ[Sco2Ih<Ӓdž"5 :S"!p21&x$0*/)+7^954!`1@vrZrw:'N2aj@瘷/`FIj1KIZu$@M#9D&VO>FQWjQ-sE[Ieƃ_KEVW`Sm'M4l D|އG@?9Ω\kNԃ&ʏFH5@I.8P 5Bl(Mĩs? @wr pp#/װ=ǡJ AwdIv&GRJJ[ʻP"T r}Cɕb׼L[z@ EO-f)oRS+y,ٖ[*HM3tNtTz "kGG+<8t";  WB1PxtV>Pg귣4 ay1R4b#+sUR9ETW}BRO- Y@jXe ~>̖+я]ϾR:JR5I&0W>}XO<*ﰺ5m$:eĪOWP|boаMdd 1,j.'W]S%RvH'O;+A(bG3rD Dt=Wtw|  0G³Af3 NQl^g0 @"ж<^ ,VUB{uBcoMwLԎ(t`m\4&ibBR. _}KD p`"6W䕗 zdGKq=#D!.9+RIm^?("׿FI_UB&tb߉Hݡdf/PhtslM`iZfy4(5 R3ؕ^AO[<b҇lw\Ή1i䰸e+LmO$xYP1bGn-ܨG7 {Dq:t$=Qr"!rnmv,PM_ PUdJ%wRzN0qO(YZ)j$Z72> ZziJcLL ֓dD~9}1ud2ezf*iیSWiDzJ/)\%e' y&J(IG=Ԥ_$6WDFl#Q@pU:a"[Z2j%ᷰ3h DgG9=*XN0j6o\US\vӚ _gbax"BԉK bB̂z&jZZ[[գX|\?+w# Dat eA+7(\!@TO>؋{?[L)Q-$B>oY e&ZhX g6bZUJE7fVyv+^[/ .9[,J t"ShWefE7Xrr\,{EwK}1͖4OSAjtKE΍|P)-w0y!lp* OüMdWEeFR$-4[{6phcț ςYkd7H_E&LS ww$9} r>Ey.it5E-f8|`˓]^8_DEIWPfL }RbW?+R2v# 0ߏ6x ;ƀc6}SN29Ue=Þ"GDEO'[Nuo6Kmڎ2fARpP8<'uC=b2̕W?(-vMT!u'@`:~55Y]d՞XdW e8i /s H#ͥ*AQ8l#)Bb..IO^D4;Q&ĕpC ,ih5URoA6:YIAe=#Srٺ5 Z!],&]I ŕZ SI9a.SY^W?A1TfD.(p>3׵5pf*L-8y.u"Xw `N@Xq5(Za2]Mʇԏ€B ܦGbG'|%^Q?KOQ<+1WZEL2y\rtKLu>!'p/4qL MA @'Laqcdn,5Ԏb),8qxthk{o+vsGL'w"/0 $Ys3gN[ 5ϚKAdw(\.v9'S谘9Hwy}" _# &,:P^]zX|+XInfBƉ#UhIdT;q,V&N'숛fD]OzXxIѳ_sG)ֿBMȋtDK'xfPꭴ|`3'y$%a"LiBY#| ,^|KM6X.(S9Lt 9=&G!oܐVZEQ8\t秷nu?y%`@,>&X7L+TzH&4ǒ5LV%ݧ]%dYɈG*;޷AY9L]yBn7@H&Td aJʝP79ZK Xf8㼞9 YJ@ʗb@- j0eDUb!o-$}wiʱxL!y X0OP%g㭐sfKf1MZܺUc\. Z[SAF9@ufbֳ?6mRe>>W}A[9gH.gr(h%1VF z3[i}B>zDJERxA-:Дj6ɮ6cݩ1{ۈ9$~ _!@'3f˟Pg +!CCrN~¤}zjzئ_f,a/I.sT`‘|mNK 2tTaV_&$%aQM Ъ]}+ЍN)_F:rҞJ:ގ}s =m>㪽t=Xu[-ڮ0rDfҕ;"\Ue09 9ԒZ`=:Jnlm;=+~\[UD=J#,j*b%^k0 4y's >0a:jod _ɶCp._yy0#Ky3Dw+~2*p >l~'C? yxKNSklVKg,Kxd0ĺ` &'62y@B^: ˈ?"ھY2lx 8 !>!ڇLPae9l#Jht$mqf @JȍyZW b'蹱OgԶkobD)ߨ>#/q')F+Ǹ߳ aA+޽mN3:k/D)MZ6PHk\x\ >J#HJUə L^_$'w2@$a#pA9E˵hNAz{u, x>CRaှE"&6Do8IΟF *FaBy5"+Ctkݘ$Mmn1@<"z+t軾qMt WSnLd4 S>cp41mŤ 3 V|@L݃DžP^Vʥډâ+p8FwA_x }j6Y򨓽ֽe*.ʶbTIP{f7q#2t.9G뺽"R*D&j6BA̒7@Pf'YFٷ>]K#z7_'Z5ë%!uHz15z%jᓪA)Fu3 ;Q#2@ZZ+>5/-D(xs*@P_YR4",b횾gU~ \F 4޻4ܢ+.yw7  Q'}Ay>:cDSL#Fo@Fu,EkvMpЩ4D& Ҷ%/#[񩼌xzNS;Q$w&Ĕ,Y|B!PBki<S#( {' ӟv, 1YDZI_"4@b1BZ`1KQ:y- 峡ImT=x8 +LctlY,]q9 [{4NWWMk@L>4]9W#yd-ר0W{ZzvMVY`pȫbe4-Fs8Ta,?ݏjܫm*"6R4>y &j:"}{:knν!<H^dqx M~3jbH9L!|>U9ٽ#Ҭ]l=%OaR#HQ)ST\)< 25|Y`Exđ3+*n8ӛ$"HX@x@;A''<=YuWvNTHĎ͈wT_ :ae^"4u ipicvux$2!U5hgX$p:8dbh٥4ioJ®GѾaeXע1x5dFL$ߤv7Y`W"LʫI];Unop¾ƾ'JAV G,񝀕zAi:ig)rU.( m ȗEݮV] P R,AǨ#{#w!BB!VdW9ۮʹ.f<ܞ -PS%}% sa -BI5ϊ%Y:6!;Tɳ5XDM1x\|^ GTI^ZT^Cmʶ+IMpj2vf*WwbsNL~||LU#C@XB)W$D0[Vy6CS!;[]LXBdLroH2 B# #vVkWCcfS]H>xbfiI!8.~Z ICb+M00K|z<ɐF\ `2fV]ǻO_U^OIQ2+ "bfۂHe ZnDUtьH5@FJ"pKt4}Dv Dvioy6X,SxI-4mit k\阚 TLK# w`>(c|Q9Q1&&:  3XǤ!I<}QoWGT`3:^;g]81AOpU SpQ~\/2HFvhBӺ85u Z8-( 1LS mF/U*>Mڹ(q,ƣHƾ 8XkU.ҽj=i݌Tb1kщPr-K HQzܐo9!Bc:5V Hu j$]EhJZzgD1u+%0$?VN&"l,2=ѝ9x$&E!z̭RыQVZ#ϫ>{27Y`6& M>Y`ٟ|l %tuv I l-5E?B C䶽 v\PRR*wjc`5jdž=ԫjYa){-WZg;Xke\I ;PԬYBy-(jQJ,ҊD}/ef lQHB%[r#s|,ԗl+Laɐy4vE#fy&+ ߉nc|qh{v'j-@6 g T>Q`{2TJoI 'jP }4^V̭[{*Y+?@%A( o#(8^MׁrI*+P~Mkj .P0je`y J鉷]aƕv>2]k@S t`lT1&a!Fgp#w}lBuK]J 8% &"8hF1uNϴWm~y=/6 FJbuܐ9%>N&-7YhbND  &(A'v/pApf*ZۑLPA7ˆ2瘬Ĩjլg~kYhkv R4@R~l6~<Bj;iC`vJ쎛 }srn X5S)lRQnyEI9"۷{}ɾ}J`[2Z.4"9jrjǿ`u*`"R/A (pd`Mgrb~[,hjM [3l$1Gb_;5IfV]$`,zy[:D*P}AS}Sz PхIO.ICJȅ`4PKt ocvm%.gn8TuHS(s:@Hnڑ.\p]|<]k!iMU п$BU6p]R*>ӳ$ >L: i&Kh@6e +`tZGHq R~j#]p8O\S}޶]CﲢTNQ`|]/ nGD&=6F",&(lsj%ۋ*ZZKJJk߯gkhW^`=(zWxt$(B_먇wU^ P-)ZC*-whSk8}2B: ^fנ80c)̔@C9ν5|~| @{g-CJ"5,v;B Vo AQ \MiǬ{1%(9c!|P>"CFs`"*U 3ܘ:0 z*XhUJaj>:"TC); ji":&H;NFy ۽^݇Z-$$&OU+U Wk=uo}U~#U)iҙ u '>/n#嵆ua&3>_ʷ;bI9N8 ٘eb&Dg4})Z.iYu*rT+)Ɗ69:6<GX (bۀ< @ H(IX,;X :AMLa0aVOg-W:"J'egD- nl#ݏD V,&D[CX{K$omЖ(!5ͧf|A=ޟʊ!bwyL{8gcGW_+LڌB1iKf(UbY þ X?ҷ!5R] 2-( #+u/t\ Ǚʿg~Lz@1^jiu hmae7b֠NHt] (M(j:F R2wY-zKB:gmKUxrIp`%eA0S,.2M-̞7tIfwII7 sgӛT_THNYSȗСBO ql#}[}H[>()lʈ:rHpV򶈹<̂bm;4az0QlZa|ɽA ۲O5jDFׂ .L}am#FErAoԦW3Rۿtn'vFŦ-iI_?(d0|P]TndhgA|x:?݅}FuH ݱΤvKG{=3O R00Q]@OY#F vI30bi!2(Zw^BIGI:ޟG_pqhc f&aY$z:7SM6"n҇nJ C[TWW)MfM&V'e/[n#>u3 b81n~8(5+ iBN#:?-|5uٝ3"W G$.f;hJuΠPQ\SDjAI1 ai :)2MR;{bAb>akt"ֽ apuݕwyj Bv=q@͗I,Cՙ>wb8 hfE[΢ҧr =8ޝ 041KK]))Btӓ!+Q,`+ZHiVEuOSd;1(%2Db&ݾ}%cBVl4&(n]fn_6MC(I`'7P 2S,+(Wpٿ*׮~:"iv%-VQ2j"(gEgOLh K!Կ -EЁT"jz|5XƮBo$ܒl0:~be%S?yvPV-!JFA8sRi'$(3"1hʫ7pI O4>ouBal~ZQA'"ܜ-^v8/Dy2fŋZ"vh%[ dz?Km Hh {λqg(h cs|(2]iT҂lK産003 q0H>[h K>/c;&?4m6D㮚gC4CI滻pfg:im aBW?I'FLQʙr)H'@"!:r ){Ocţ'@*!"FOD1LS,B zY 7 Z}m-NV$* pUŸ/=o8#˘+̀Nr dmX$W7wO-dvݩ5L VzJIc*@ W~!˛~HV\۫sE6$&c >'ψX@]+ ;a\(ݣ41Cr w–T!Bbz^UNjK. (h3L$yqFLͥ5i1fmίL+8N1eR豷ٍ{rYE,RxL5$my}ɏCi~ rG=?K("qŒ[[/ Gؑƈc L݊uiofbX| XN ėY " 4Ͼ鹴~TRZN|: 1_[_ k{i"eAyp3 3Ɂ˜] 0\h/a[BS+)n+bz5S)ERPEoa W`% {?%p? >݂`8PHyYbVhM_,Jd:hNt,SD1`z̀#pos״9G3YYJX#Ezt/Z݌Ϻ]b`qNtQ8X955U4r^Y՚c(݂kC-m@h3 xhZxBzE}wkw'Yb},`œe0L+N56<_C;FR@mMT@P'.w>BZ%fr?K+ WP2vu/fIiiT,2@! !a2X)bДGk,(tO6yH755[c5`Itt_ 3K'cD.k5~.ȆHvݨ?B؉)sȲK)<Ɠ}󹂃9? ri9(61,dc4hQzBL0ZQ$v@g mc,s64}?Vj5p =`ਚo;LD& =D^DỐ13B%ݺ\3~|*]zX+K G,cdb9k*OVfb72eE$Ou0؍u0%؀V~>r&p@l6L\1+;Z R: ߽ʣ5vQQj#}vPi(/Ua,*Ab#'Q,UiʷakT'nEla:97F^8c:ff>3'mm_IQiN !Ukݭwp'O O,R3=K4qn08Ov_ob磠dq٘zp3#$SO5y!olTR[ǿoxeI?||4"qX⛖yi d 04 ˠe^ fr쯶)IvoIcW(SfiYKOL馊*?(`--9`'nKDc4C9hH"u+o0b9mySdx`&B*G:N iƎ$dRM(\YaFcJ 6]r (W![+ ݪֈyYo,fvz6L]4N`{{,p#HɊE ;Y8|Gin  $"TGΗ,No͡j׮UDvZ=5x 3 ^7>!"}4> DfJ3_h0l# PQr{~²$Qz70$f<ѷU<.J%hS3 iiW&ri&ՙZcgEE, ߽13J|L t;mE96JlJQ"JmhyU>%#_d|Aèb D>Y Sܩ0qĢ]%hG<+xA' }VT#ٮE2#b.Ԯ)`  n)poI<G3)TmnBS=cgwvhie <|"B6p1~Nk,|y .&[$"U PŮ+{,׀VFj==50ԨcVyX)@7aTZ}ggDMt B G/0qVbVl (j-|df)8cD26.wh)ًn"W T+&ޮtA\f4?rߢIsA$6,k,u Oɉ;D-U%Jw{ȔG8W UnTƩn/ ^LScۄo*qogzR~Sz~e|:"^)re7 Уk@bh;nIj)tvt="b~D:LפCAKSߺP 70i Ԓ$tJw$H7\9\N ق_.h'>/LU2&Ӭ0SUN9D)j)("ruИ~".l, +d|CLH/a4rN^TsYɜ=N, cɖOͭN&QQH~:CڠV b 1 5mE?X04* V_`ڞd}v0S OF U1pdNYLÃp>K*}UzҶ  a .יU6j(Ȓww-T%Ip^[:Z/ mK:Yoly_غ ! o . ,`Ƿ-KTjŒ<_[M։>SeLd*OT₌DyQעKo3hoaHtVw6 j*{iDp\[. RX ",c1=b!}^P,F`Ey[2Fs( 0n#q}((!Wgšclw F')k8ZBJTY:·w1\ 䞺sǰ_Xd@VbNľeetm݊*]l;䏷ZΦ~Gm <6]7ZJ6]^p˙;l+[#im[:r}tȰH@i޿WT~wW-yu,QnW<5VFe>ޗVejtuB/uJN[txyj~g5$EBJ[峟^j(l**hH؎S'zBdnMXoo{[te*w9SiQrPuE[S95_0EJJճRMCsmi!ȯnhVB]]5ЖjNK*iP3ũZGG;+vכ]R S{\5]̠6c Ѹ MYt{Z4ujLl~Y$gvp^OVd=hk|w[Zw,mokNv먂 @s391cCBرUX|u~쐥QuNv/JK3xK 8*?@$τy_ߟ.&_36U@+ܘ?͢<͂"uPt=z7!4` BBPw"Bk27r%כCW)vn ]p#Z*/|.K<41fUe 8Zׇu㳎d.?yը-de,Hb3%~BP,+Rkdᵍ\87w.C:srr ³Rs_)M 3bzD.EB ܬ>%nlU|>-oׇr^e11ǍƢ2~k]Fm<@viBFhIp%(npW1 [ ?/ԫ{(.$!룡}_fjb"xL7>qoݳ4~1QeepBq:C0·w=!oyϓjO}w:ElsttCf}a޷6b''p??+o(ő7޻wpKSl )%taNg Ԛ걌pqJ`".7ʤ)SdEʺ~p'%?ЈO7<+r'iϷ̕ ܘ81 %?Q4|(OF|q&CԗJ lf T opôk*^HGvU ́WƌM[PXa,B3ddp |IY^ɳ=;u9tKL'(1]MYؠHg, 7>9:<8 qEf77 ҠݧQA4tA*l:7aYW gɺ88e )oeqpxx0xѹzZvhxF4ﷻ&;;.ƴ9"t hWOGRaW褎lf",?V߯Pc_2ѿ߬2jiBa_. ˡ c(*hCwD?NW8 /тP=X$jZ߯I3NVmjWu1'+/1v~&n(FKP{2Dv8@9r9y"oB _ό(r8;3v$Ynf- ilLXdY8'^ I#.uOq]llUThdqcƚ TB̋Iyce|$sҎN 0@ޠ.Ѭvx˷3Qd̐ 3m׷][K)T4Gx(a3j 7xh_=0^ĤOأc@0C-:K`-?3RXB4lͿ\=q,$n~rT"iT!vlpzz/q )tт߰ԉ9΀- (yUӽ;w |[ͅW2ћk{Ruوx?vܫ{PN*Jl:n!"vݲii9n`3l#uj5 4ݢuQod"g5ܴ.ǥ}Ѷ-a9z{Q̎sӚ}tEæQ½Tc6tm`k!_bfoສN R\ty)$s헭 47sp{V3\j;N>ln6jXmoZx1 4N/Ƕ Gг#td-bPg!Ԛ ./E2#T8!zs}$nUùm8cZ٠y%ip-bLT)RPh"W%Q4"8bG Ncz}d 4 |8 ED(TDe>2\&J~ᠿp3EdR"XhO$Irn;(]BaWq`QP+ (d>5~L0]#$f`[ UImbI&S `)Ḷܜ&_3аbE8[|,u#59a0Xz.@YYw8YI&DmtdJt&>qkI2&gJ|'`:>iz Lg>H/7әkɝ..b 8 H3d,:vvuDc۽o9".~*NFCX&2h|(wNE*A sq @L"F*6fBe3x +0mIym%YCK&Ѡ¤aQ e-;o$+c P.{G~c Ȏ#N׋ sO`9Pk2ɫG6L=L?Vg3 @ ́MDM46FBDtgaJT8J_W@!ЦKQrSQFeb1"7GXDQ¼E)SQJrFA\P,D0Y}2Yɩ$?"VXx~ n΢z`uH@JsA۟rLѝ&M \`bqۼu#}"y$@Ls>8Θ\xŷlѼ^-M/Irq2*Zp|IM7 fWlK 6xt>@ !l.1ϓ4eC`q^`I.: l9&' `7gяh &K}O!fH9GCK9Z0BhTU̧`O4]sgͳUpjKtt2h@gST-Qx-D죍SP΢SE/J%QU7h_jU0n/%gcĩ.qR̓Q2G!z*Q68zqn%OjO`: a 85;V6'*- dmA"lB&XurpYr[-[ 3C*_td#T_ ; Vf Iw֙WGC려3`{>[yoC1؜ v0-ANQ8#0JiI;)<'@:IHfi\$0t:/ dEM 8D|`I2/O\TsAT97męvOTÆP sə `!OO4sGJb*Lsu`Bw E[ #+qI$G*Idkr%ߟX5z7ogsϹ=ːAI)~)!/w~|C ?[¦#%?a7rՓ-®1|x|lZF;jhRzxnzm9* bMp2)V?JJ1l&eǢgìqf=su4D)K ڙ6jeEy[M="^\XdE*zUf*3t+4W=J'Bx%Bl\Xb \UddtĞՒ^_ujqye?)M{}Js ގwmv_p|=oN"O@lR=fxڧ:J(*q,lȗ  L#'b<;+T#"LYS,# KuW4@&/g B-/-잁nEµSx%ɫٿXӈ K77OCO :w"Ss_aNKl!m%ԵZ]sbo(Zam<_=ՓͿanD`yys@h3 GMOvard+ڮa:5YDi;]_f`Xt=WO78jq8S3(.**}B!0h?9ܨ㯝_> :|J5g؄p퓤D LuHO\QoYCM0UwL Dh+Tr'uPx 1InGIGCTA7zڤM me.g2>bEfmI//$\i}q(V.Y RqtM ٞ"`uLn_N%}ۑIHvS6TDb\煉nu!:o5/l3:Jnu%=՜':sKBMWh uqRm N ޽gy=lDOm5AԲO7+ @b>\W R%1śo_{7x񋑈E\cϮNǵA1OEas{@啓4D&e>+&7JdJF`agvQSsho鶛:M "s`g?JrT1C MKXArU1 8 sEF{_J?VoHIdFi+6t Fu"QB[A ٛz 5MrMEL3m MgV^pl:IB_0賦@]h,׏8[&;4@Ɖ{#)yt#d6Mi o#e>RH l~ֶCmX-i"8;VϺ擤8J=TS^Qq58DۚH6Nlg KIf^r5%)(SIeOE7f)ԻC2\Dr'+9jJ[V*5\ʲ| rUrZA i%ȶN c Sګnee!;1[ jm~hOTy>ڪvLlh&㺽'`GwR"73($S0)!b_jc`6P- l|nk⑬ 8rj~{bf}NR }cLGKBRƘS[I}gцgnn𯓃ˣ3:m !H/)tH}teQ+@i0DD! pT«PHmP围]JH \Ɨm| ^%+~1n_Tde-raHk@ ,9 \4G88 edt/+g"o!_~ΙqߝҟkSqck+ͬ bJ+ xe.<#͢q(#H1)DaK_B 7)CED-'pٽej);% Li6|s:.6kB]3Ff9lŗ8Wg5G. {=Y<-?JNoj٘Ζ6cUT-0W~ksL|YiBNL[T,vߖ+Tn"og?N~AmҜ!3J]طwП /#έd{hұx4 vCRAe]Y[پ2#r6K7C+BpOֈwΪn/΢'aF#oFj| 5FʟjmJ ANJ>&qF"χT4u|TQׯ ( 2q@)Yfue[rO56bɠQNasS!M +ٺkG༼Kc_HdppD ʀzEf3KPTӨFNR-g?m[rHԊHȉru.螒|>Ks-99{{,4C@?j{ qLJ$Qkl.gp<G֏!sG<Q6ʞ/E.n>aI% oX&Sj>XAC oLG[9Ib1‚i DV4ƴ}C͓qм˺CQ(= t 5">Q _'nHk~B'wán#Xo3u9у#dV73cfDHȲHWwR(X۞£58g:9O9zkV+2ى y49!JGQ-5yu{'V(o8i@A5 Bqm AP 3Ş@%PUõdvJF-48 ׅ"Bmk׶u2(RMoK&|],,@g_==*4/nv\2A^Su>mi۹:pw4Zp>K&vbm6P boqfl;q3uO (OMCIsa%VK2hlb) bS(g=ŕF@B̜"D2/xb =",o8?.<<#A#R'|p1vF]2[)ɿJ0ggo);[HonV_)gOYIG܄1Ha/[Cթ8*@AOaz%wRT &5N?~Q&xm.#B|2X>",-k,X4vgCE"4-B\wgA{bFy&|wѯz_\ߗ hDwMzT6]!YU\0dwpv~4M*nY/,7tEJE #qQSX0F |M- WW\"3dDVH=؈P>J# Ub1iX~6~f7nw= _uewpbR |4 kKCv.P߼q((tG=ڌ֩ 3wvJ)vNvoi0P@?Mx|%@zʯ<}(!'1j:v֟B>gnY"$S-dՐd\[P¢hͶFƇ0p(軻J-W!z( śK2Df~IEZmЙ$kp9n{T׼"[?=nw qz)N7VDV x/%,T.{}@4pe}^tZP[svN~81r1o;A!D LgU;y)V{)F1dp'O]3wLVC ɩ4#JՐ-ǪX-7ݚFcane -GϻQ{<#6xPcbT46Eɾm,X*V6V},uxڌr2D89Mè?==G}%+fh۟SX5s :-Z:/\[@'9w >/$IsIǓ-0ś+zC5o8/c.,gÝ;j˛n~x\85`-V%ҍqǮbiŹB ͔laP8K<ϱѥm]jWL% f̉h";@0LGlУdΥIt.yLYF0|/A`pL+}(C ԞbQ̗OTh# 1ъ5 S8p1LZ`aO/^N%MY/RYY@:D+hlʧD#4Arjt.a%T"=>SԆ:Rw D6~ވ67Yߤb_ݩp%L"Z$[+唴=m:MwKVF09iga֛}t}gŧ kRӐ QlJQ2`k{"A`WW@4t?eٲl!KyrH[L y-ߡ] zpaٵ%]PK G' <[oroeix;7Aε GTԧ-| ןO _8H;MzeWy~:1K[pDo_<i}˺tw\DV$4)'wmE1~?+ W||'n, it vd2x/¸98AtUR^;?z-r28 @=0D;;'kg{ 2iG"9G'mֿ-4Cz)ݤDPj/@0ԗ[ 35/QiH}=h>)R.c]߉ڌcZͼ@m]%:9Ss+/=q3)+jCdm\tI,H ,wq|@,ͻ$_>?맯qHX(acP#JV,Ba 99] <1j&{sQ-gm 8mZ ̙XR"JcLKFGL$P em7ڰ,ڕB̾3k5s=.;uO kޱA:䞃crq\m_Ա1$( [O-!q]\=X :`^#I߰+XyXCY^X]97|heif{2(/K>O{|CxF3*i?7h򇭢UTӥy|C j__ELcW]q?K}#qa7f ͥ㟾xxp<:swUS{/G\ul.e/afh ]1NF$4_1Sz#џ5c%={XmA$א-!߽8BJF wfF5_[4[d}GDgi3j?_zPD DK/w#qk꿒VV\_?kѹ/9Oۣs.}uu5pr;SEs\9峟^93 (hGͣKރ5mUk܀]r4G iMzK^/;? ھ~tW{Z4ǚ=B4^WR]).}\r4(n'GccgMpv&pL0yE8X'ː_l@_Ʈ-+Xy7ھlX6CIRDRO^ǗY3 K2f%f&IPXZ0bid6||&~u̱/hg'y3θ'g}}ggWWkp3=̭矆8_?OF@H :tZg:h㨮N=k L1S2B%g`rUy3ڛE]ifj>I .H+ 4/1g;Z=~62+0u%鸽l+8oO_BA`| 4L KQc9fIHNf1TOKYY8e.u8EŚz[k5U njb[8v`us XY`@~Ds@01C #dt1TfRi.A׫#zօ> 7붡.wt}!{ocq꟢Yә)X;dS.Wmi{Oj)8p\,6bڌu$bCg+_n1ط +ڶe?~x/\87A ~m |g"8Db"[D~?h}瘢ᢁA,%m'/BÊhf*A%c4^juIx %{n`b5aʀ~}$ d9C]4` tQzQYx7)CR_Bxhhe3`. Ɏey&zY; G> 8O!i݁DP;S'3 Z'ڧv VJx-[J&D5d 4 3ICkɮs}uəs+E8Y W׶% CګvếMP/Jy8!_7Xtv.ƥ1]$:Zܶ RJD.&NҰ Pbm|©R^Ljch@6"( eX$q2U qL7#7$ؠTɄ]Hf<)~H"keQ5Ћ)1RLj%̾oLmQ+J^] 2yx:h tɝ>nx^E86DkľqK-%pb~0 |7_'l]}{졇]'!khs@s3exC'.^Y_#_ {ȗq)26@bς^FvA$XlbaWhF2uhgDxk2#,6f6ӔY3ʛ8_X'H.8}B:7Aah{7#앦 ̓]ɗ˱8 d "K`^J}!OuK]4]d_@'o ]$\ b ܿCV%˛4RZ_E\oЂ_ڏa([M`Dr V.Vlfcʅ}49m`pGu:4ȕm`14( Ith)hf$рYM闇@F@ *}y3ZIϊ6f{l`l[|m^; *G LGFahA~CqlӤf \-Q2IBep١:_B1ktPp+wE8gIgO6#Jص#iocˁ6ZErxʳyv:MtRmb^I&!ɷfpt u1jNeIlꅎڱݶb 6^2ydn;L1vq&!Fd oܓHbN\ ,Z,,e_s2]%F"g'GK}V=)n!C(P>݌0>̃]J>Ta`ߢ0ͲbEwSVl̰ʜ('JP֗9Zt-|\([+9ۧ7J2椎\yᮜʼɎ6n|':K +uE3gOE(7c!7PmtYDb]"̸Q">bep 82j΀!&G#k`"ԋNӗuH%omZ ӥؓ@QlJM0CS8\Y /I}\ LYS2Uը;4C;ӫtdH>e04pG6kdqٝmkDNrD.*9T5`_vC-+tr-p eWV rMe WyJiȆP>9tRxZ'3-xhČ2D Q,2 ĉQ= A 2Hrg ۀcP;qk# @R&RdHY]ϰ2de).CTqf)a\.ONȐtቁEf> &'ؽ5 k3땰w)4_95{ŭ~vܱ37%'8l˗.Ny"S5-SM;_GcUIYYPM2_-EJcA[ǬKyVoRҊu_ӌĝa:@SI#ז:t< "Lt@~ G-R[q8ekx*3 hU> i3ݤ[CI{J]Rh?N>;ط#դ /sswYq)7I>քhH i6=>ř)[r{O>{`S )"w֞Կ^EB^w%Cv%DI.8c]]jtվQ:w2eŔDTu$Y QK+KiJK{T"Kw9H"m\ ܎HڧzǥD+wcaGxmE:?eQg'p-cT`Iy(Q6= hdewAR9DzekdƟѰ!A81,6W|9Vȶ"a3k6Jґb"hcG89 }(ѫB>6DY)[ qI] tB=okM eY# յ%[Nܹp N}Xom˪F?}+ Ϭۄ!QYXB si#젰c!Ti%oE?7ȓVp\#9];ZZGBG~L hPƼR~YՙkFRu7'mbd#qnkB Vɡ2gD ޒ3%Ǝs/Îy8y<+WvwF;G9 ĉM%%,3h$'3$t@BĶoGh-J bGgyJj 6YzyG)??-$o_<#[[zCmVP*@3^9/X^aD2cd `J%o`"L #BnZ1]+!tO* POw00C3ӥB$AUHW<䍤Kg0G1ޘ2|ҐmRQȱdG뛵zb.gk&Rv<[%E$+@;Xxd)Gd5 lEJ6Bx;١ǀeѩ.oGvUIk.)܊Ϟ+
&jtɃ11d?磧%TWWOwݿsu1*xΡ K߭Šn:3!­#hlo/9wjyLlvwVRUAu:2I@o]-o)SvMVXAFI&院TnK6Ku*E?׷:}gZS/@׺[F$&ZCaw5) R3L\EQ$OlD0<؃804dMIbpų'w^;˷aCHDЎ/:!Ln Sx7&e$Tla~bGmm0VH,> `.rGGsY_h=1E>|PDcm%1q6ZSI&FzŃȢKJP]"LVGk3@}#wu`wi}fℾނKYb\[aLi 8*~F4M8 A $$=o;٤x)`5Ra  pG3^;ٲvv}j&B(i} # r>huE:'a8=h_FȈҊ\~G`ݎ64~̙9v?Ӌ' %#2zx_:="rrUEhJY|냌.-ͳ^{gnWc ?7[aJ~𷯝?"GC cʏK9p!x7A:i\Ad Ѵ`" #؜ Jh{ aK qYe^֕==NdLx:(~&x, |X7;tD^>2 4}pb;-r+e >NGr.c@qlIHH5M8kat?[1ȣlC}yzuϞYb5Z lɻ>|ʾuHȇ7h"ފUԮ{nuxJN({^=l~t"xԗ&؞bbSB4rό ~ z))+ɫ暫Äl"!Ӂ) h ?uхE9Jm Hαb !w >gtLMd)n~.a wX6{NyZMH9,$ *<&es0;[pF^H J( . =3gv>ٝTAF+db%!d i}!sjlF'*\R~8WqyA!xӛHz$m$wcX9CMZ\ wn0T"=yx4!MpAI&WAZ&PN{,2l%:9vLIK.D鰾 L/}3CD6Ņ9ǔC͛(j>e[cRM C8[(_6G!GI5riįIu?C N5I}XUjT1˽ /ŏ{}wKf* R+"tC뿶˗63Ճ>>=я`t ` %9[{V$ɑ /l SD6dڴQ.v{G'\: Ƅ4) RY5" u O0,7aDIdt23{'=]< 3N)\ܟ煁/Px4IױHhi>SBs%-A8OrA_>)~4.rin=z>|}荳O_/F뽳w 8^?dƉY,2=dZl =DB#T>!,w^:Dy2AS)2aqCѬ0GƲR )rEf?6:ܱXq4q$]-qL-֥AvNY1%G\QWM  zbw9Q)l> %w^6IcV=WƭSXW;dq2-@%XK]EPE7?>?Z'' }ؠTA m7ƭFPR|U b7ʄ/0T[@S % FP491 JŸ&,4V<{\<:v ;u661Dch;iCL W 5ţEge=P[F~1pDo&LS]$> `T1C.c;1tS C C> 7PnGcs>61!j7P " n"y7?q굋fVэM$/]2F>ԍV[Gӛ*]]v/n}y]/㨟wY: D'8t='K654Jb`Y47M,\6k\ StvIIl7И.4OȋTGPq0n@$g9F+mchHDIB<.5rv.Oƹi$CagRGE08!$.9T1Lm~*𑔲RMMC%'n;Z0>Sۼ2Twn\}( mR}uj>QD5;l70Fe;_La,kT`-PC)9ʮ%$[P HhGʃ]A~vR{ۡ Ț:$Kq RF80SrTf)5_<@`Oy :c0ѻӝZgT n26p; ݈d\ˮz(Qm"keՃO^,}x?wkH:8YEi6rJVr?G"/-`*To߽/ !2>\XS0rBTVc#ib8Ɂ YP6*51>IQFm_A,:2@P#s[ n+w"k\x8/SڋI\x=-bNX Z`5Iȣ[$bՈkD<τz14'Vo_} 7@dq[hw dG.~u{q(YZK)w}ykEE04^;psS/l^~v{'XZz_>Ճ3b5Z#D RkmRG1oFܑ֏2C_7PFn2("Lq #BA|Yn(( T(>Jk,멌1q;LxF FC7D}{H](yx.Va;/hĵ1X!uXpG)3\-nXj~~Kо7/;dፓiK"[dtX5,rG֢WH^`iI5 ]?ݭo~gj>C# 刔]qBS>NF6aXC@G 6M$Z3,`+c A<[C%mEP\:t*s"3 R( W5(1̩ʦ`h35Rr5!z0:𔳌hQԃ"!!)`HG|RhBFn[ @EE* >wO*Sn )rjVSޅg]lqmHn]t;;+A_9嫻 |7kT,IlG)4Q3Taڨ)~b@3MFJK g}TۧʱyjmS u;{O?M'nױ±:جznwN__/RaU #x Ѯ7l%u_{ΈUX~~w}uob%U+Vhh/2K=J=GnT\Np R9]nqG]SLh>Rb0&<=niP%Dh9%ED84ܱ]&6oi{R۰CaI}HqIW%JjؖEcZh&` Qj7ab4OQhwo<xRU[{",ӠywR2+)nSv+o\CވNœxe{}oz|gb\CІi礹rD-*\C#E/ tes{8{xA<Vm3B."'7Fh.9\rY>|bFxB4DD\1l0\kw=y91y6hZÂF'vU{il$FGz]jC:u!56^AMĐo@U;%s[m 8{I^5 :U)wU~W:<}U)^.8#)oSkS$`pzS% %2T6^]9Q%UQ _ pӣ}v3/2id}T8ը%7)"GB`hth= S4vG ΂<ݝ0[UiT#ܑ $\ S0AA)6t:fTYu?¦v쳇>kF1o}}}Kn=bi6i(Xry=^ \>jWQ7Zz>~Y'rwgk.EJ%<f`,&@O olh3%&rN2a[BW`yYˣk'j}U7+b}2n~~77k@GT+TR@֋*Cj^e܃t(ʾf;n:=]e")"6c1L4v_ 4ěnQHc9pF%n+Su]C_u8oy&@3|l0uZ"Ш$X X/ v˃Fy!űvr?-p߽Ol qV(6w6gZݴӲKٮXdxɪC %ʾ^{K,VxgWkVO\ Doo\+'':ČLv#s7%ڨC"` )ej/S0!8q ĐI$i϶$}L}Bq{tv  1NȣC2OĄS?4WzjfgX #1[ʻHG6yASou֙K%$zz!˻ZVg[p+76^ͣ/*q13Z ` =qoԥ*L\0)CLk$2I('mIQiaI1zJCiIVZ J(4%y9 Rch$r]$BFƨF6PBVFVQM8ȟkGK\"7R:k۟[|KjcKIs`ZZ@+@H(vɇ0BI#$ڼ1팁>7?|̡)[Rk>+gk9~ 5Kw̙Q_=k1@˃ /pwo|xɪTƩ/zN{"YP;OmS,;&DYbaCle(ٷRh:EIkQ6Bb4)Ф "GOUskzl)-lBBܔ>w" P̣Ƃoſ QTu\xrQ-U$-=ArmMft}Wqx4sVG|Ɩb2%kkiF%><;Tr^,otdK.K/^͝syup€E({*v|p"[OU"+'I(awxSOC~l*^zLzf-̫3GueHʝH xӼ`uEVe|=!u6D !WQsn3d:ށQ;XJEK *FflE;?a0=HC i;%GY]ːh.lzXE0U%JY˨ս3ڽ7@d хȯ)fD'~:zU$#S@9.2( Ny"n̔zӍiw f؛@B: !ԄW11J&< 2ƙI!C[DYVXO-@U;8gw7%LԴSL7ɻ}Ԡ<=>|XE^(o .mw3J.3z-‘ na^9TvL5$_OK (|PhTg\y_p֩ԯ$l֑F#LP ج( C`F;'D$-`C*WHۭKA̧W;U)7KSFJ8;q/E+F3r8,2xw^L>{cD׵j˻iU &:V4ybZݍ9mFyLvJ6uӑCTtZ’8j0]׷.'T4[<O%}(cQypjqk- vȿd&C,H9j8¨zH0zza*͟~iq,qFF "g3fl"}d+CK~}mD%ftέYhdtVvGoEA?xE5=_w%xHE~ !QLtK? WF88k['6xɡa'|kvbQ0K|*g!#!&Vj&}Z-C;io"B E™Ӎ䛛HIs0Y9( ZD8!$=rkv:?I z׌rMH WEg;.BIo-7٧AJ gVGGA3E&$pUj=rH2־|?##/ªWs:#.07.nVH+%n RQe}WoXHhQ%9U){yh`xWt/G1hj_H!m8ϙD- -Mw( 3âBb|rP  "C%LO6XCY4?I 0'va"I$O[ Cۓ1u F6Nr=+$[!%0D4{ӦXrnY(ٺ{!{#vy +fnj?Y"P7ڑ>Kv<%L΄&giQT[INåE[9,at!Tb`StBt8 {mTZ8 0p#ň̷y !؊ `s t5.!EuHiQwΖ:u vF$T6..!c9hyXCSze UYs-TVxC:?TܞQ>)d#3r'W05'8 (%4٤hȘ6' 0¶&m XLԑ?䏡[b>Wtz[(*|DjJA#.r%  ia'tj Vp̠PoZNWZf+휡D˷?F],&6ZҼآRp.ʸJ-zƩ+3Ğ.1(JC^(fŅ1bbjJj,rrrD\,hI9]Y+0_D Wcr653dz+}=: `:Tr'Ҭ5v`$% \@GPR80Оv4Hra+yh#StSkG!E +q DH="c%LAχӃ3:YH4"ԋEq{L O91-l rAޭͲ{m+W4LwqJxGzEҦ*Nrqm%kz1upJd_<}eM\RI4(fbbS [J*s"k/!daW ^?NYa^=讵J=NC7lOcȃN:#Qh'QEɥbEB~i4!ꇤhg"fHt*zӠ/5}*ӐGN;I9 8A'mQ$vw :P!W+Vh.fyQPdM~xyY?<-~{B`;\Vz/-ERbэwQJYk9*\@ҾUpwq PH|G i.9@:B6 gC"Q{hf2`3Vg o)+6ݔ=|z`#C]% F(?s%]Lz\m킊i{E{J{K"Z\`p۵QsunL.E5qƈsUS=L8Ad̻7?]|]ڸv6>8."SĂ~~X+} OܐD"!c~UUG"DI!"<tJڙѬnfKI6MB 赅Qeb$A$y6 ^P"wll1} 'ZGXfeO 0Dtu"d?9z&O|: :v"Ք.J$A0JgxcZIdUK,|4U7˃|wTxm-WL %u[/*6g!. c}ZlQ!HvER6X(AэU?DY0X1C!~f=8o1U#ZHrz Oi,,"CK& HUθGۿ"{,:2Ῥ~q"|>mi?zhqO?%ŜuH FCCASw*q^\-f녃;\n·WF ՜xqh@G_XU(Ck1Cn-Kg4:t7C9=߮sUIL~7TR\erV+d)^2n"ΠEHOZZy,0pokvc䣉8/~Oս$[Uף[}+#VPmur4A{ȝj 8IE=qvn[Ao8˕S͚ތGUٿ=׻'~wc7b^毮o|x?hi{!}quܞoJ}5xwG?8Z ~KwoNK?<8;'X[5Ykչz]/p?މCC1d c#dS'u`QF Sf7::ajd7MݶfNX OD 4m] ΓM9 eRr; ysTGpC!K% !!BHB`< ؘf3tLwDūR<%#ׁoDjC$0 =si-%&;y ͷ,y%nBh| 7b0¾#>cM'&?An_ G1 L0GYdk~`ڛBPbzO>=]YVXƼVO)n%kbDTE -f&*D=_UD`Ϟ2ow.*8`Nf8^+W+zmpbstciaQgk_gv6K0<ה&'\y2 u曎9=[M _oO>L:-}ܶYm[3NAח LU`ܞm3Bmd=Y^Ǖc`zk)77MubgF rWMx^OW/ۓGp.AQz@nH=1 6R~*Jex:xkzhQϬm9pvsYYI`SrA'DnuZCrcOYB`no~舺ZݟoLxmg?17@LhwS?z{c\rnэGhav84E.iUB* Mnh6#/:DGMٸYB S|'ĸ1nO fk:la넅w܄##M8-)7 W\ࠊ)ϓ4D;2cpӒ015C3s,=0l4$ hMib>8'!0%1&k~TEL^yXٱ;̭SE"H}K 8`~^]$Hzň-WAR Y-+)M xI( ",DtI+O6}U$P2?A*ae&ct=m\A!aXeo 59nJ3󊳻jqr,r-؜&"9rWe+eM_шh)ZYSTpU1u=hJSgTOvHakt;IˇKhd Cg۲-Qa<<_].v{p j 21Bi ،֌w kķ{z`XqGqzP,z;0XE䂺 {,wqTv."@홏7"Ar*zԖlsӱVUVX5 1Wzo[W'nAs 'U_96zUkʩ} 䒁Bn߶@KPa"g ˺a'%ثWsԺwG*<{E=Z,Ixczi~ !g($XP29yyzʓߙ ap}v 96be]Fvpsp 71!X&Kߪ,XE[dQJH[+8+mP[Ud(Yf;ba.--ygi2^y=lLNXl`P7VK7Li50LbMx_μߟnx1͵q]e2?옑[s"1<ܻӘtQfx5F;bo6\QiU,륯?#:itb}2-E 푍'񡮵=.YRZ`mA. `:3Pd)v8 zv,`rh@ TȝZQ^lO eS]tȄ%ʫ41󽳾'6j0iak-* &B2`ZR!fNDɀf~}@1; tw *h#fz NAo +({@ypڪAS(4;z[<*/3`&3ȣ,5PCZ˧SU#6e5تhΣ$ǜg.B *H@!qEH6U%;.mqa"(&Bre&|= ElُCz2.q 6Q҅ WPaSZK H=)FL,Pև Y+$jWߚ*#8ofҊBtiծq<cұK*dB^菍XYWGXe`S Eݜ;bX(VIޘ?Q\ VHKgtEVfxbQ[*.Y.ݜ}OF@, >7v[>J+?U?}ii%5x:r+,RQVJCfmX ^?BhlPNMJ6[;q&JE8pBjhT tfq O -l&prf%n@3)t1i|ҽ]RYS-\7=Fq|­"WLʀfpg@0beˠ 8ůF;O67<}3%è]EpLdOb%:' bFIyI͎&Vv( 95E<ڨaܾu~HV\Q],h;p@znn^ƙwGL t-"K\987@Il5'k2bP+,d7]^y[7"G\naH$s8pH6{sG|tEP^i-±[8.*M; Om؀s?X![8>?'kUz9(9XaVLV<\Ȫ`|IF! M>&QK &*t &Q-xh9ҦXLoVTȪaOp5tHz ;w 7mY۬C o]aZ(liaUXVKp_Ov-_\F7_i6`;uw<_dQ0[0ihRKQZG+?J==oɧXO1a*7&R"Gdw''-}Dz?217A`M@d'G&m9@alRN"hwN0XpbՓgÓ1H;X1;1d.v] WFjdZ8%\ kw?8Ĩ!1[ghf?>'131d c_{byM׽"q/]|g ;z jmU!웑-lgn+Qk!UFO8zoƳَFt$+;; &^i3#`u^ٳ++IP HX,|a4O{\t`1C$f{5Ū>'GՋWFm doSA8r WQ<*boI@/:GuiP2Me^1(IO:= ʃ^᪩} M6NJ$keм`D¾F4NohS$3zXE換? jRћB- S׼-2Dϝm:=o\1&" 2dPǔ'WLdtK9ё%S̉$`fx[]HќCq]Ț6g gocwSD4ґ(C^:xF8FwC`Av uCH@%A|7\=&oӔ[pN]5Q!8d23+q݄Z&r(@d- hXC# ?>6Lj)Qfi >W>?$^`4˵b:MO"&ES:c€rBs,5=& '%݁/mwg* !2ڋD쩄x*mͶ%yŷ]7" ? 69-d1s߮OS$V*2KnzL~lb<YST r5:dQA9{:ACSL71 2!y e6oHAc#0&]q׊91!U sW4dRŊS0CBfx,9z[*^*mvK<9)-׋tДΛ;O* 4ڑ- `-Tı'G~~)T׭sO>wί`Zt=[(i d@tOD.qH p #m)(14H ONd90"ؒFaY~ ;ɮupt[=1lOw>FL^ 2GVqHWK 'vɯ-C=admմtB`t/&]&~LGDމX7EJ>IR9$c9Z/'?ݠ-2iLC"a Rx3Y!'~ānYXcS`nRDG w ـ;)r9" ruufn9EH-gW<9p|  9]:!aۘC=ZQj?jɚ~P2>zK {@k ~rXd|zc[x{őtrc],߱ԽgJ';WP~_u8> =b _aoi DĥP`$,୦92s8Ӡ, -G H!t߀ nO_`+ s" Hm*A V5l(Fٖ3:7>މMT2dNF,.NӶ@0Q-i€Ƹp z.Щ1F:N!3s w"["W,(5b) \0P$%Lei$9Jv`Oz4A[7q@}V4HxdT=ng_Ő=2r{ѱ{ц};!y'C$6WςBSN(2K7LJl+9F{#Vȝ]ZY 7^  u*mߺދɳUA< %buܤškUb>r)Kan=#-)D(& ѡM-L$>C%^M唌4꾒ƝEm0l,CQ |%ni<~5N`zn>`.zW (da (Chן*M\%EcQIԱR^nE~ЎzXK(/NFl~sa~뷮-ԔRi 6 w 3-8үvV;5-T˴9Nk~ s- D֭5 F$d;i+78SԄ[)ݱ'_Z(n4H#n3J>fZmLu9=X17}ּ^ NFȉRb+"v-o7o=-k0FվS㣗 /3ۻњv>y2qKuKOy4jGna C*$5 4B m?vcӊ\(ىQ5Z)qn:^}&5]ˣ1:Of' 9HN4<ڵqqp{˜r~ 1L]cq ^07]h/kI1Kኩo\@e]١ն~(5;2/!oTw_۠J_,ڷKGWN^>~)})a1kُv*&'C (?/2yKZaX0: 5$kӣ̔ g?ǷCc-'M[;b+ ;LA3\sDm_In*t/}'ud>|4.2Rhv1!k7<P"(wU~ ~Ӧ{VIE\9B$D<$G#Q\vt,;wWɑV; "{ ,:gǎB&8#jl0t_|xxU[٫I=q;n"ɀmh~$b2p:U,D/P[B1ˆmhvV_קvGIe^C5}YSBMjs#m̻M@sB*' \q龚ܸFѮR[W)Q=]ŽhunYCzH&?kav_Lm4SK0Paa]Z)B|*t7J5vqs?]l5eR3Mn2#ڥGj|3g-i.O)!I^XÐ"QLtJm̒iuaugEnmlOjo0[M[{\C+E/S歵5n:mwb]wQtJŜ_(d2\I&m"`L 2bҊlWV#;v*]-wV/8k/$E6iV[g2֞ .ړf٭SA:ݓTP)YCSnaJIOMIߋ12Lx9+G 0~reՁh~zOa  `/"sTRiidD9)c䜩'#N1 &r6~fQCU~!Feo4qB|A>WH"$K-6l4uM p 5(5y@ 7JewWv Q]ϻ%2Jr!biwsSO’S/>ߣaǶx էGk.->>"d~BOcF|{6fsܕH]c,8UU⠍⏟ϯKŮ|stqo~W~`9PjM%Fn~!*@MV٬7M ih%(Tm6s_Z ΀KKjlյY\m-6im#7kF$ǫw!M]ݹR'n}TXtwun,Ȉ2K$uH^ ;zD'&^HDwbTj\ JdB<AM<k=>d-As/p5ULѩjUbCw{K(&i'?fVW]] BFLrLJC>E>MB$~3 q$=$9D ] 5- NPڰ5!!ڮ #ѴaM n1ut2#0n RX'YS$8g==+= B5.̶nCݲz>j.к+|wFHukWOȻXwմɤQWT=8%E[}]qUZ/g~"ŶŽĻȴ,ZRP"wW0cUm@w/W&?Tw'w ~GŶzFG'SDtvKoK"k@N'/:,'+y8(9+$M&洤O4+4qNĮ\͵`hIHbb0qClݸ"oΐn:;I 83E@\δɡ($F:0 ͺS%D*ӬdԠ! g,,_;x}Qާn?v3elo M]E*OT;{xE~9"ЍflW2o]@_R  ->k1@ rJH!K}{hSk&Ռ DZ$&ȘmDBÎIϥ ݀o| 00bk VTu%Sz L% RBOr4Q%{%`\7\6O4}t_lus0 }KHpXqe(4@EWvvXeWɤ*qia"7,\YYǫL6ɏ* U~zW"ԭGw6hXkj3Z--62Ԡk '6BY0& V>g^P=F 燚I, /X',33>@a -ƝI- /8Y&%*6H:n^nc")*S11`j> ,991;( Ȍt3}F$C^Z3&L-Jyף_tɿ~T}]mj=u.Snw[o=B.Pqg{Ќ e5: %ƇO"8( <7M-K@[Z6uIC8i+\MZ8%qPoׯ͎D7(aY$6C$N@P""E_ aA$بᐎ4[( 7MRFjlp.:6*h3qB"< !d+M4 HeZiJ\zCkĥ}K=Lϰo%}C~d}z.a'N?r׳޹y*A,){k=<$CyD'+K9+O>7+]W"g(ш~}mk9vLuƼ%gkP1[U+pirJ7t;[:kI$R гv7Hh9uO]N1t neȫs}@[y,ݎPH " MAv6 D#p-;ݘ,ؑRA;i >kD{)O |o\8uG \_z嗮ޥSۃ }|-cʷ}/*nޫ*L_=+}2-y$6҇=1֧5OOtb\%dӵɒT](C-%s54I\.,9; 4d P]YML'eKlD# Д/z&;Tmks򌁢=2ȕP#Z0# HkչK{yE#;[\$׶ nO~@}!N˯x wO=rn]]A[dw OʪP #^1"wy}^W^Ã@6|5%bs` X6a6ȫC`Ș5:nd?. kD| ~N:ad #׮ MS`:qD1;5윋"ňs.jE;K<me\q(#c(69hx8(*~{1C-138ٻ_f!bAL$+Dpwʋ7.LWܺ-oq!(f rѠ镏_ϭ B߿w9c]ׯׯyhx"ϻ8i0a=tm..Q^!ɘ9 E=ijxbjk Xj߄.a? ND@ K#!Ɗ'E}DWE_1b0> "h)|F,qRܹ}r Qrр=dѵ:u\_2}}3tdcai(W{%4ɼ4k-Q /2rD[?aJķ EݶB2gJePrwN}慟G4 AE;je['վTiz5"XMA-8!:um: :l o?_eЀpsU Pr-.$5x';NvДYr\.2)ɀNQ͆;_$B_D~yι#<0-TWB.4f5cC3>ur:4Fk81KYf}7eOi02)08ʩ/KaglC@j= ~wpǏ^t 7hZy94_={]Tr#U+!:JH bEyaR^X[#z6([Ԅ5|`* y !.qm*up M/^T6:}Uha҄DZzui0Y%t\"ðۘ5.8>vܺ/H^KI̚A͐\;}<+HKe^O-Hl+9G"~|PMU*ViݝQinxnWlc1qpB KBFw4__Wg>BJUN[W?so?zux{[l類]'t\&g7C ;d+`d B=SD";Q슮ČquAO6o{ TKuu.UlWa!cwPH5xºh50%@d}<Թup-aGv焏c )1~@Vi/zGf~yN4<հk^^;uo_{s7Es cx'tAD!eB\Mvw-2aR$zۿkoN=7K[آ:SrxG v[ڲ@dËQ"!)0ų Z (QAT@KSiTTzVq1k4wΜTpTwZ_ljJݔCI@4.hxBRZe*H:,|(%9ZY±ȷ%QL:aOačyKmg'_-W~}w5i+; 0[bZ_I:>[o/wOϓ٭ȲC≬ [S>@' H~k,v $cw_2۩_x rrJdL=UV0B u)%tca\ m# yqtn I}9F6` ,xWUGPkDۨhpRiЅls]vkxHSt|"s|܉&u$ $]meO27wO,YN8NvΖyfUfuO{6ÆBV6 _2 zgΖ"Gl^yt̻SIqhS)pCrt|H0b[0!MRF?T4?k8DM!8 6BƽqdYO`JK+"Q-m|C) W0HYEe46Xd^M(U#yf"}w:?.9zO36I_w%㒳 1Ll% 6z۩.흸zx^akzp!3Q8Llvi"kG7R-ʨ8Uh1ɻz025F ,)8h`b@TSء]C0qg+Ԏ#ˣvA|*Hk|"{>z6JoBCsUpUwԱꉠDzD\gInf/f{x-!HMn r<2[Y/D\"xp('ݬy )N~0 ,3A?^0 +HDصF\ G*)'0e! 1cL3.r?4P @ b)djgjCeW/^ >ܨ8!ӄA`]/gjCXdظ%P̙)6g9E1_#QWqncT\3\p)hB< ݂5 H1PD{pϭC~vulռڙZ9oBdAGہ_=~%KB^re[FYjNNB7S]:GdTlL1A  הtx:qIy5.Y]C`Dȕdp0 `nǡ2.,$dF KRv8 JPꩤuzgr:Pcn`ȞdWO6fR-dnW]} 2;h^t}?0_W-`J7 %)\\;iνBV ~حFT專NnН~sg}ZT~=>ʯ%M\ DDBF*HfB@"TIX(|}lRv~?)"*JF@,F!]t {Kw9nTA:bްȆhY `Hhϱ14:5ۅ[ףګq;ԈX05dOc_~]F֪FAu<IJ| K 8B Ez ?> 7T9_$T^6 'GnXl~ [d5}>SE]j>&aodtc5cA>RFm5Ka$]R2[h&)u#L MFgٛ4!TA9Ġc^RՀ.T/6:& [ 0B*Kzt#[|| f |:u9j;NZ̓\D\~8EJF="`u$R}D[dʩ2n'|ӁfVI}q͛dt\~sl*05BOS_*r[\a 2O^N]c'"{|HXM۫3$-sMlxdޜQ[8#oD=#WS6k DH,^\0_~E,&1i"nNm>A "<J 4|}ؔ: f Wi?r n=9ewí>_ҋSWvx{=>ڹwdU̪(]Ol+$o>i-KtyRg+CXaf " qkE<6OI["Zd_`&A$]$;$p:HV2NV|$vX+^ejLo.~-]:~΢BE_=~gqs~9W|}y)*0*'g$GI z^X!ha4 ,-Y-l$h׎7MPq2xO7BcHƧ4r0DH`wT|zsHl5)hS.Bҙ%^\;/&M<3G(O L GH7vo|3kױ"j>T5|hK;,WZ>Sito}bNgS*(yIw?Mfaw~pX\ivwBr_hfx01{TQQ f͐q<hWm$DC:tEȕ,#a1&x@lmC#z[4euF?xMm`րKgk9B\(( FYG(B~9~KZ@a,5F!-a/G=9iilirr iQ f[ŵϴ 8O^<.n-TS.p77E,]<"5F'BWJUKOͮ2&=]] MnQ.:>lu>ۈl2daS}` _!Oöcj†R_XI%}xa;V'0|t:_Xdq ӌq!ԏ޳g"<ŀ \b6 'L4- eD#nHCזE ֶW1MWȡɍ%o`9~tK<3w_Y!DqUEqw:<z|uyR$SL?<[{e.nBGFA;5)H< R=A$8@5s [Ax4R>dX=ɛ H\RGCXq[m FfֶQ ѣʤ+ t70N?c lwZZ}p2δ+웍ԑl" ѩQe. be#ɣ]z\Gf:ݿg7wxg/{n?WΥ\<~r핗YdK󼢩web_5iz1YX߯U}$wg{paog!_sL7pqhJHD`l"EFm~4S2f m猍Q"PHu@# 1g{W17C"Pz-؂FꅰnDunuh`,2\ i`ӱ,m<jĖ_|!48>lDg;~'1<|t_͕z4 +Ni"3L' -<~["eG uSm!1}"#͙֥_-_y[`A*!mg,mj c oDrDIgDY0AmR;މ,G$4\r?>v;h77/H5\E`MԹ"5y 6@$+5?j]dF4!> J@= :ˏQq'؝ msKH1&!&gIfnMh*f_ˢqBzUC!#TMF9"qix0b=a3_~GjD &EI% >\|>݋.4%|rmk;y|P39a++<.n;mS7uS{U~7@3FV;d(gO%I^ )dvlCGGbn#D6,e`ȁ*i4Q*@[Q[B$7P@;["ҙȮݿ= (!5 %"%لz"BqRf>r +l}S;M'WNìEF|kJh]w˩&Wh^4Y J!&@SɐLqIj~IѤ5C;#5o)d$ؚ68ml wf>6d5c/qCdR6&(h6قZG*RV0 $&gI{c-f>oأv&P|Ano7;"y`%i1< GJjG~e~euC@SIYb>=*b{ΤhZDݽRnp`E3Ӄ>W)h[rbfJG;OJ~Fr (ՉUꋎ#%zjĉj7sd1ib\!Mcv0!c  x>h˶]XddNrGՈi`1 "8C촒e"2Ty>z,=q:sH홐{pߘ30'$R r5HD;OLP?{X R@qζ8_B7 aWM֘!K}KVJ_k^\pͽҏk}0|A^?M!L1UĴv5Dܠ!koLd5b^m_ FD㠊˕ -Xlg>ʢ+vLyDxV{D60vbxlmTj(KO')IXse8JR9{Y=dA*ށ>"X:J@*{[M\cY#i$.EšEBմʂQdϯvvMQ(e]Z07wbߞ9L\X=>51BFEEraV)^51D( "YHX5kɘEv6:v$N>!.!? Uhk ybtEc?QktTޭ)"M=i8gA;ۑaGWk7OnPdF>ZD] ̍$:(V%Ýp 8&Ǘ+$%ݓPko U!\ŠO}iEqxd?s~dʮ~_Q&]-:EvӿBråD*C ]Ńq$.*JUh\p&. b<'n,b`rY;HQv zF8H{8ĶcyynfncHݥ``5" D5-yv}d}H&;)cZ$~c.H/'nV4gH^cT1F(c(76#n>ȷ>M䑸Ak 'lP&| &-P FBc pMv6Uf!Q#wi$JZ?EF+G֢&r'/7ؘ+*AQː1tǣ.6Wt߀rg}ѺΒi%w1l# m3d1:ܮ;c_rL:IQUiwNM^> E2DYxD CcQ`.R8NN_2YdŠ:})ƛ-3vR4CB+f{Of,|2FLP/W2尝,py$.ji1I֠*rmd]dEtunF<{&lM z#Րf%5IrcXah+:)?k6/s2 -֡i;V11QvEqDipM(.]Ul7o=~^Bt Ԇ= ."?}ň0wXL1\KG׹8;*R|ϷbEΘBLxPvjU Τf"%µl2Ni A9.%0"uT@uY#Щ @$M8 3XșYBeSxwxUw?TI]\ex2l_-1g ^WA^JI7k1vOD2!i4۞ Stg2dR>Ν5pctͱ$_YcRm#'R̽{c0 Zȏ7_;i,T:ԷD^ɮM!+^)c Tbd?rz_Zu)h7p(iJSSSZ%ܙ< ҺX$Btjei¢2/L 8saƜP 8&]V$6Kk-q,6Ayg:2N! ~2۫/d m~1~ "#t:ɼ&g$RH=xr)YJE]ݫe5nk4 d3E$| "ľ=RH{8FW75J%^G[j4Jk1,R PU/Fygܤu]NX^+"_E]qwvNJNhx}XTջ& ¾y?V5mr6΢`ZȒ(dudm҆}6,$E 3{ï(O $$/:0D\,Ii{kW߽:OꚒN%L?ןs݈c&ӽ.dwܪ\) Rn0KbJ]ZB|^4>n]pn$h^q h'XNi1!ҋdhi ԅ'8T2tT摄,G)X&b8REa_Q ')O!oi2i4-9P8liJDA\`? SNL fR,hOlmrIJݙ~xE8#q^ބjk58|-%C[ꞴjLY Lsb[7T@r\t 9ug2+QȬ7sHjo8It=$JNQqISt`/1U >_x_2qwQ/ׇt @i~Ȟ,sYGd"6UwF\ͣHOrd 2̨mg+k@3[AdBe1f)Nj]LR/KϟVq"z Ur \-TG!9YؖB Mp߻&޿fަkar6M8c0ëuhf,/[!wLge3 RH#7"msbAy:Ym#=`qONFd@OBOvgl c=C'%GZߓ]jI .[& [ٴp$X "͓Ɠ-p )!,`~%1J#+ aV\)$j{5F)Fl^Wǥ);sd׶HpR2^o.AeI6 ImT,NcA8tZxn%FdL̯)f{ H@`!`\v*dLL@m$GO]d彌/xzI1y :s dI-  'g#^w-[^it#eی9Hw#{I0=[ӓ+ N.MIN`6RS!MQֈ$HT$?H%RVtoݔbuh_b.'A!|yGID)9 fY yDE#P՛@&$O6)\=O0X02gtNT6ǎ"GhKuv7p3q; F' CmD,yѲv&h$qɼ롏G0&c ӆCJnj[z住Je* byDb~iRr:Ч%'8,+ۜ)o(@k0.RvyczeʝݓvN7~RErSO.8_[HeLv2՘ fKwM6G?{>ͽ&{hȶ aSubGc hJ39kL/!M!HBnȢeh2H\$o@$kDEcExDޔl|9Qg 3 s։QnCcfO/:3TS/9"x0/szfiK;"cj_N^~dKl%f1\b^>}ta?DlN㰒3A$s7U!|m@%xpnDI=/0LkEPmӄM 2;fa6dA6fOiWqE Y'UmxeYy Z,cW{ضņ=uQ"=?9Y۞ۨֆ9Dκjq^=cm1f1W_AH~.λ3eƖذtlܯ{5`a]I9.+Uf>aIǐ ,,"xyMݯJR}FR}A4/KcՅYɗw/> nF:;\83ܞ6jK'>Ju곋gvnqW|g{sci:{G24$H9cͬ ٻ iÁzvj=LWc1أ-Ҭ^T6^'+,.5ȌSHUoKR-|ŇNަT`hC;:Qǣ?=$?<Ѩ_~`kOwl;|??U,%nY]RO*lyolʂ m8p1m ·Ok"DxoH x2w֏w~}H,=+ jiw^银nxW^]yV^>}gը@0ٻ?qyS~<W_zpT\u`][厎qiQʕ; RY9j^8E89%_/.YZ윔m֮(ޝQ`JG@k'ئޅΝdOSZH|>=" (K7a­y}q+nlGy@wGIE˧wS5F| Q֝{a ɃBvgyBWȫݍno}Sx.Zz{u&~y,_sR0R#NU;;_^{LD%L $'G6y9kiA4hKD,n(nSJ81GĺV9u.]i +8nY:uw[=iӵ$/r`(wuosv%m{3^p; |yo rGvnwˣߞs1 0_hW&WopS=z@s:izlI†(ΞnRʏ,pm>~d m;?*f[2t^]\`kv{Ys!o\_e~}2\8-a%& ^:sk>ivO<7ƈW8}^[7=4h A, X:C_ 'e: H%ܩK]Kp/rߍE)[Ώ2Oؗ|k&x1D4?7)eEUxoy^v[kpuCLX<&Σ=4N!. w!Ƽ89!TIx߬}ȟ]O8. AO蓷c@^b)MYavҌV_'jf)f;*] _w󼡖{ ґ% ^Otx|v0֡$W:$Fj[*ڮ1.MI4 O/QDJB(w~}l1T%)6!Tt0T'9l&Zai`.Asv~Z:@__ܫ(_xwG"Suڛ%h$g==Țq[ڷ{x ,dv?>ܖ~p'{oÃwn*;pdjA:U} Ȼg&'⹐~{5g_B|AƯOv| 4WWavIX5j_[qǃ@MFkM e&3֦N=xGcNcV5^ǗVqv6O*6 C%QN:qm8V G̬aXcQPbp7(`+8Gc{|e Œns8; ݈MT%F oNߏ3^ZK@ ۃ?0La-[pg<,_SoKC@3죷jyzEc(znIߌW@Qv|2~װҼ}щ0׻O(N{=2F\M5K;u;ڔ8Ej5.WZƉӬ @T 5A_oΨOi 4y-$Mvͯa=a'Θ#t3!13=kƖ/nSwCՕgܹN3K#q̰ +a$p!3/SN14?>`tE&$vʅ`ѐw^wI D7Cu] UYAj vi]⭆?=0uH \gi?:5{??dj>γ+=Tfyo*<}<+_Ρ#&*cW7_:ǽ?z<.ʱٹx +@P"=7wʯxs~Љ\ hֳB.gov\WˎEqV07g  HnܰQ(Q K?+CTWTW9]]zZkP?"שofǞ }ھ霝,\.LaGCM!l0TảNA4(6=8+'!(__]_U#_omYOa8S (&Jt]hi@G3om?|fK]cLɢw,rN };|/THe)qކev\^1Ht ĔH"|6F\u%4I+!U 6v#a%b֌2$~DG-A)'No1Ы5cS1J =:ON5mTР2a#2u45`٥_,~^ddžƄ>)FjgC% z|%1;TeFF0e hv@}yhH\"j{GKS6!HX "!wG3hB+FPی2p ?BrBY^+>8><>7xhfu ȇ #m%A=ixfHwF2b\ zֽ{Gf8OYWک鎽#+˓Q%,0П-u!k]Hh'o_a0Kφ R=dC6F`ۡی˽s;>4*'(a)FKΥ-2ɽEH]8EgNl,W1Wf pd2~kWcItX S`j6]M61nx. TL2 sl$0+ش<5`#[X7S / C9F9c4MjUN}wٱd}|vnzuwp{(@&pb)̵R' F)w&s\vf"Z#5[s<;-̂>I켒xEHFVy(I.Wv$½;*%'tqd­wO'7>SC%'޳iU qmBN\.Քi&pdF IUÉ.$ḻwq#IzF.UլgW *nLN{MCT^I\&NR^.pfS(ZȢWcZ{f\oOՑLC豦\$br]0!^ׄw%29[^"ju^z+>$SUAQW-V<ݽVG:5\WUC}Cx߈3`_lzTjGs%dBNGf*=Vg#gzۖw>!уiͷ1fE"V zIoPC,RS鄫V}; ; g7F{KIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/stackWindow/icons.png000066400000000000000000000100531342332007200240000ustar00rootroot00000000000000PNG  IHDRL4gAMA a oFFsu A pHYs X =tIME  ';̏bKGDIDAThZgT IK/e2%P߭}TJhp%|gÇ䗙j1= sw1sڬUN3YER0:@F6+kbC,?uB@bvi>L(#D 1(p<|&XNKO0ԩȷ-t](O `D*.Ϸ\뿔Y7W}h DK3lBGېא59cN[\Zݒ RmUZe%H?ߟ`m|&+$I$,Nс wʼL.Hl}IUH@JDR#y'żv6=$Fׅ{䤙F4y X;m\p+Eٖ/K" 5:b (/ k_"uNl~ '\ $\/7=0pR[Wb/Dy#R gn,A'EMatZ*ʑ$\1M $@N>%g}w ϩB':qwtbcݾD cD8qN1%`ׅzWf͊J!餕LM~nb"8`5s 5\>6%6%Xő >Ջ:F!-1.G!>|f %gw#nWX }5̊P̋>xm\ n)g%EPc1}\{7 J4*t0_>đt"1n"0LO4ٛ3M#_`A#f;Ή8e$[,Oq:Z _Dj\N0v+Cn >xM(ݷ~e'5'u7Zqt?sEjưyy'n_Wzg[Q{eenj]~9=}XMK:2)ϳ~y V؞࿩-[ZdjAhr+rs-xVcc;~k~0`(Ri>}X4W:[g-̵rH7K LqwX4w45{4r&(s ys^ ҴGI]L![9b:Z@l<V.iu%:ʼg~R;ow'`؂}^&{Nv+Ѹl-s:Y.CZn/˿^@BZ#DWj  f0_j36Bw%,aI&pG ]@Z괳.J&fMP>Qgvǚ4>=G\B`?7T9uS`hto|{U,wqsツݹ{5EQǻ'f62utDž R>6T{٨`Et5L"8/@n"t?4&j욈 #5?)҇<*Zs? QnؒQ) ,K==ut(hѠ%5"Q&˚!z,E`naHYA[PtW.~mrG`wpCgXDZ nvwg>Nϵڧ?([N?͢=NJJ]4谄,!m0[[B۴`{}:*٩͒܅J)!7D YdBB>9չ<\ ʩe[rF~ԅc쮸jdg[6/p(uhSª ?z[_:EH`LWۉQzUTr |8߼ #83+sӹzv;ck렿5zw63|K?oSP،hԌ$9iq{V,~:"3b(Ό|IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/stackWindow/info-panel-0.png000066400000000000000000002574651342332007200250760ustar00rootroot00000000000000PNG  IHDR,M pHYs X =tIME/hbKGDC^IDATx\R\ɖ)IQb!)4"4+ѐs*í{WFwsD]c ?>l߾|O-uL_3޳9qikrk{+jWzg{]ݟ;}YKI `̕GO^nM^{Óg{zm>EqsC7JKg}xP1ޣ┹mMTTn%UU֮*gU@+7\g } }oo/?_տYj뮜X*h/دRFu[KTҿZgՇ͋VG/.tׇ.^ ]u[͋pjY5H79[FM[UC%D"WqzM.)_E.]PwRM]^ 识DU5V ;;::`[&*Jz|$UD%ӿzVjԘl8R$i$o~R2}KgfP!ѡwۜc,ֵfVOWa}D+:$W*Z+Cįzqg 5i =o4Qoz˻< :Y CJgD:C] m1~ejaXԯ+ 0)հ'< WʋVTHOL 6nyҀF>QB"jVwN`)`;pK>1z\ӿ*K']*;0)yZ@;k zTT^k6P)A~[; Iʫ 󺾂d+hZKF%'uMcT5Y*K)ʕ212 Ad Qc[K9t~ U*BhK[1K tN%tT7q|@q~|ܳ@C0RQ gQA*N/[d.LTD4 d Ƹ.}L ,H0tߓ/AƧ0p-y7[ELO(~ҥls)oӿ[뻗;U_?}xND%U˻(@CTD\ꬂT_sRSw(IDvﳳO>su)YDN5Tӛ\m=N/o o31?7?;2?`? YRg" BYv n)˝}|( orcDD4qޫbt⥏/umot>ߪa!x !F7 }+^)4t}|7dۡ >"(4=lRge@8~&y~$xP9D')*r)C"9 PPd6d(%LDk0=0B<*fzQySFzCgNh}YN鮔R_Ek.MC_I/{+˓ݞL`9y9ŇMsxTVz8tnN{2ݛ)gTUߪ>zZ^h-+ܹ5RQUoNt'xH;5Mm:t_CԢ˴Bҵ헇foeܢ K?JUA H򜪱ȢƭTИʂh;f'ܨqklmaLP $KLvLk3bPQ"э ܱRl5҃%!A$**$4"ҥ0cs<5 Rvf&dAئUla**B]O;Ң0N} ;jn Ĺc=N92+s}{!Un 3+beW+l<#ibɳ癔Zz]꼊EweRb3prLwNw*~}ͥP}כ0TX9ћ)rTR~RrJTWgɫKMRp9=D堢1S ѿJ)W)~-:5EToŕ%\ƍLZrtM[̈́5ϫ q5RR u4PSwSTUͽLa3"vTڞ2}gDg_:+t*{k=_9+Ar}a :"6~*oF=xX4QjظRHm!]!&U3[*O<zJRd˂1A m1b+x6b43?P}[бHXOtjC!bNB' 䯩%R߫({-JVyi!1 ,pemP`k 1b9EM\̑ "sd:AF^-܂UssY7FS!|<&0fLѝ̝@2fѶf>,lA7ipuj>`;N228\4$1+sP>sFUykbrt2iFI[I&><+7W'Gu($͙y"(m:K$LXa`lm"˲ZQLrehHU xt#FĴ"&T3y"pe dOWҫ7=SuܽJ -{}qƥjznX! nis0w%\7K,FX걃>Uq(CT{3ٔwl4%ەSR՟wrʭl̙Vx2Y_y~ ? Vf@~g'iQOm !L˫̗sL͞J g0etjdInn >ֳZ4c :2?r%i[߃֮dKE`A^hhT z/&لSqՠ/Jf_Z&f 9}/=U0;^D=DAaF:kB kH:ViGc_.tp@'*£SY X/@8 x&5ឳߕs=|#)XJYf^ߙ$"68E1gmX*R%ix_}Q|ԳFc|_Z6wr{^}!y"" w%RPfM%+aq1 KP$7Xxsdp ?;nX@0<-CnZT,NFy pP4%3ؤLnUmT.jytF9lĄt eXʈ]*6JxG>V|5 jbEQ 9fhDx@{ Qo j)t"Y]I]%* h8j48CE㳌o桋VTa堺c&#«Dq;y$4ǯ(ByC^Y|G"#(,{ZR' X #&=&Le5%a .ʜ6^򗬍JXP(k __JS1ô'9/IYƽX _PcYm]S($ђˍXUIȣg۸:zG޽!i^ĚiPK#ѰY/YKngS^XIcD{7)L?" G Q+G[α yqk"O<oP}wk6Jngi2Y֋iKM@!-n(h/Y EZ8scQ^(H_uHy!, @D5F05 F װI3uaY!@L/wk`hJ3b{_Jc @H\, ~rU2Rpc8Bυ[!ذRBA.FNį)fp6ӘƈîC$P .RdD$ژ (%#ujA:߅#:m`Zٲ&K;mD G, 3NDYbf"zm7-0p+ɃIRhJWonh`K%|dž1*$RV,a5eg{dt}=M gO$Wc\ &zz vg[Oq[X7أi2Fx*hbcȱe,>dz3ia`5k@2ٚA^HPMUqΕ=}$-KϮ݌Ic{aUl$ѼG@Z= yLBD-xDkeq $ZoZq|5kLoz}oĩ*IBcEfk+?H(;FEÆTfDc=[giǍ2)Bq{̉^(+T alTyfH (Rr94{TJD~+lAM%x奜Az !/+w1JNX*u]9ңKg/"f5"rD3)b|HXs{9hQ+0׉ԈY;<#(ؚ,t ;kQ&L5Q݄*{Lw% deCv7'# NV*$ePf,~v?bgG6Ad_<~@xx?W>dۿ>*2㚃j !70-"omkpޅ~PP2^h &r τD#phF(jY"{bru?VhNXbel:[.0fܾWcUh(ȱ}f8SvXCN\ 9¹bW tt]:?ݶr$Z>`~aCd `^ 쳶Kpn(7a*YXbY@-93,G,7 R泶kuIAI~5겳֧ſAd_9]QlCd](\H#<0 -XR-azkݡwC]({N岅FURpD7c[AAN9b>b=WYȱݱl".FB `€1a)ܼdz A׳ _!fmB˥RPB4I5N?;yu/Qr25얔M@А;d Ѡ19!l/ eR!0څPs鵲|>͊u$hɊ$"jޤA9Xt9Կ4'5)U'}ir%aV \ wʙ"G/[*CA(2V&ț=:ʦ%VG8A 3YȎiݱkI)N6?6I,Z3l7>a|m-$D Z*Jc{U[)|<8\o6Q{d`_JAckv >ZB $،z^h'i!F2nڋ5PVT~IGAz w'fX4.cb^Qc 22]5=.^} EeWf3ys 58 )/Aឳfx&8j `y!C@n4d|jl͋MzټeQl2T,DjpXؑ3u᷉WX]- rCq+x 3{`  OSA, B>PejE[Jۗ9p58pD+r$AQElDMH&HPeG& A 8|Ċ׋@*ɼLu Xl^9!]C |C@%I9@5wo~{ON +T=  sXl4*K?vqBڬCJ?.*s2W)r|_Ї?u>J7?ͷx6 ?62~.r8Hraч.půo9b\>gQ0XCˁ/Gt(/R9/`-)lLt:"~ 6r\OPg* ?a!eLko~_~JNlzbuЉ 8.S9!2[KͶ 49s|BOt8o2j1HP<9(4q1Gq "|~.٧$j2[Qr>6 Uc eqY}\ [xg{LJ-9,'^LrvN9LδPTnu@M8@l%~85ss5<,G2p`QaN^oͨ/*Q `4(*_L:.ULyŸǾTk+ZhwͽG>cGos~LMO9NdX\ qTҠbg^+=*5u,Rc,^lqPrщACGb$dA [nuX/ xI5|F+}hR{Y<|1 擼8ǶHLNs]lJ9gA9t\v+e9,S"Ji%Iqٿ3ɼmQ{yg{X%BG2aĻ&EZʫTXNt,l'G9p*1yʒ|Bw4yn[3TGɔ)V8wSN/K?ߧ,1,%Rhp7I#Ye 6{4Az&ϜxI P.aثq!NkyyWEGASxAN!j،ʢ2vDceHwBDRV. 3Wb>`b#eaϘX#l[d^=g%v_)g=b5E(\yⅧM:*rqݡU"@ $ v7wi%ike/MmT{<[Sq}vœii/kYLC`4~imH9IH>j4wc=K#FDSV.˵JZ=݃96a]Oձۣå0QF,K>T)3IxOOx @$z@2i%)r%Jk\OMTjݳ y>a9ޛѻraŊ FܸD8fSƂ/n(1]h@G:'lr&1C2 &0fIU8-wCϩ^!/BzRR;!8Ctm~q5[>3Ap t$w-NwǼh%蒸̕u"Fؙ/jW]_#4{o͇~kM ĺҙ!7-?ޔ,9paS˨|P4ܴˌb9&2N!4LT`zRzU)!m;=AzR1k X7bxb%NQ Ԫd=.)Z集_q񕃯\TT}jsL'qǵUO> 7nzɿRPW[Wݸj!R23DaY)8qz^fkL 3oFѻzek`6h ->K Z7K+)({?Z]`{G21[óUdnG=Ƃ83&Anhmѧ>dmTb^߁uEb49VKͻmgCnƃ& R4߀x_8s}IҚu-~Z6 CҌaT: ;QADZI \#>P}`hLM" 5/f:) $z?`@Ӗ14R.nn]9w2T o{8fˏm M%{԰8mYJjVr;Z[!0NMw!0G <.6kƆ#RK^m;FdNQ /[Pps}؞/4ȶT0cp/a2ȍf*8p9_Qt{YWpXHk:A]d3?pz>1Ǭ/"hI.EQ瞶X' ߗ$;` %}ox ~!AU>9(DnV'(^N\Cǿ]#~{u{~my ċ4 7׫1xI?r\cm8bprw91+ RaJoㅗ㵛k{+.X@[$ڮ'}QR&ܘ7]-I_ to,O!!hp'6Է/B+E"Ǖ>2̫0L&I}\ۓ"!bA:FuGxf&|?xIg s[)-1Ȫ ^|6+VG*K]pj3B3 % E::çyF?5NuNG?j#3s]GL̝j Is}09B1ˠ']gŞ}{#􌰤||r\0&@4nR%+阄 ˶LQ6PC:Ք LQ1ȒIG-^tgD=e\W>7uH03;W,22!eq*&5hM0ٖC +Wt,AX X*-Ƥ;Z"zda畢 Tp5 ; fm}.Y ˪evBV!;W۹TL- ViR9L5+X =Yõi~>io%_K^wf7~{໭اswыr9L mx億U%ar7 7)Q9OR.zһt& ꤃KVf`}֊?k$V:ܙ@uYMqE=زdr lt. {Jxj}1jl|x˛Y[~x:|}uM ~xxsV>_߶0b~uFO:V66OU;`tSQ~|u7C|0__505,?p}`#qԔ =hvVj)5࿇ind=sd/63XX^krv=^Y38Kxl#`N}҇zScդN&u샿b8/ֱ3&Dr8=ېfG,g:gvO Xy11\g\%uqbW  qUt7_q8;a/uxU/]xֵVI#'sNfo|1`s vf _2x-BJpqcwu;&iB}Mhrƪæ0a[5CY,: f+3^R@a|܈#ၶ%|lj+'Ʈ%7,:}x NܑChxL# *NXRRjޓ01t6ce|=uωdz2\4rLuAfn?~yӷO~7Ėn؎%?uCF?~3_u?a}K`% >1xs~}L쿾 >nL sgLF#~)| ơ[q髫?}y7W]%>.?]'W6$qCx{ﺋQQu-u_a Vzo|tg/ >lλW]ٓw}?rϟly:{畞S=#= gOpǟg ĒQ\c0+|p^_aL~A7/kbl{b7l?< vx~)#c76gy\v'g_'ñ'l\vpqdvnX2%]b7\wrUqp_,qy?]_[?KH6ܭƆ9?}IC<5FQWbsaj"#EƔD_JZ$g@(TܓЍAX b3uϝ\XTQϙdxb8Uy\78,F,b0;ɾc8MN V'ĘIyLbe  c؅%|I‰ z%~:HΔ5duf 3RJKz^u3 c n̛b³:;+ {zRΎYl ImEpȮEIT>[*?NtYŷ Rl E\Viq%܇S_CMAl".v_Lb+ Yf)lfoM B>-d"@1#8ȖAVFxvS qm@Ӡ0yeQw~7d"\$N=ӯ,sbea킩IyŎ wLfL/cɲ^ilb\Ê0}=7F_Ul,Zj,R$'!{$mX1{H!ӔU˦Sz3G%RKgX wQ~e3cz9DV_@21!H>c69-\ +ףq]UvrQʦf,; Y;>"IjW%;dtEE D LYyKQFn´Q6J1gY+"p 6RQw["~싍5÷FPjek1nASƘo*K~)hӬ)˺I11)ɾBHe"">\&+rvJ-&b"}SonpWtSAґIFy`GOfP*fs5;y. n)1YUdZ5I}-Bo6 GEDQ0I+ st45d!1}DiYft?4VYNR˧DXP2%n^ "c4ؙM,mCTǩ-muʌ[6'!$QpD kKװ,N-|3ƎO,y (ѐ9q=qW 5'E[+ 8RZ-l19枕%# aew9j_g o5K.lVD-qs 6@(SLVĎf0AViN#ӆѤu "`Ysϸdf٪Mʲ:bUpŤKNЗwᎅ wFM*"b3EJQ^TQ=42[0gW; w^mN_D&/GNFa1/1%-ܯiy%=q{Gv$fa=mL 30J=ڒo==Gv*PV) Eo*O>2H8{ $pcH77fŋaD04ؒh$?#'9i*!-$3cڤiܴgHn:#[q>>̓fQOl"l0!rC}G:Je /qX &K[Šħ+axj8 Zo4A!87gS@O᷻0L1ښޮG?ā)Oi\a&gOlGX]Pעm( eܜh5xj*jGx.Suy[䩟ROB)#V`>ֲADM1s@ÛKKHGroMw|uOݰ篮[_7c=Ѝ-Ȱ>/Rײ? /;Â+7= NZ7xwR R|#UЍ_kmGQ'/;-ZE8i%_WJar1hTk;^:E(_ CdQ~ʈ:cfV"Bۄ9Q7縙hqz܁VIe91,fK FDC'd Z9+ACb6šxٲ1TOY_+`T+i(=eqOLh ԯNm2W.$ȼ=hL4H[i:'iTx=~JFeeFۓy۵~OQ5x$E5җskQi*~hܽy߆> ݓX*Oxf8{+0v6'91^`}Sv´H[k$h>M6@>s2Ƃ'Gkbζf"6$fG^}!Q Z':,$$&fߺok4DT!fܕ494Nh-1Td(n#RҕSXp4|tr)hg9xeQѣt3{p=o0*ݵǗQ [tXmBx ^2 _s SxGюNxqy;,6A^ә<L/Fi -< Ѯܭ*q|i &h,;$oPWpYZ oҁQ1/Lbt9 Eұnvnּh:y$D%a*flAc 7=ƳbNL djj*lir͆Ŏ %٧o.U&pܑ 'Ȍ'2g!Aߦ5c׾Q]n<tU>^<)*? `eAjޤ(ʂ p(oa/ ,8(jeg83#V>"ƩI@;X9FT7B4-o$.6EaZ90vԮ~/G2zq!(5̟`bA1x(NL hԖds#C?,G:,Q%SaU'(1me*tX%g932ݱL?tX,qcxu Gp\'?5Wv(:ki"mc{㓡J^ds F#ҥPv}_+Yd #iIVz`ٌc$m%; "ԉKnz5[nԭBQȗt!qbyٰek>QL{1YL[׆vCe,Adi?4-%;aF/ v2c6h1E(tMƜ4܁-ՒuEz2N[Z:lPJ?C.V4>&N n`ފtl)#tXmOY)1~8D0l58>#nXO,Zn4ɜ-OS N*im'({C$g;FZZ[惗UQk;PGdM@G%0Q!6Wڔv ͪp B?aF]>YӮjDP/AáOm|(wKG Zxc*P{OUK:xm#D>G`:XWp|&nztsj3rV("?E'a{r}uDc­+NE5б҄aaE;9UmƕQQ8)  vRpo|~և/B,9,GUgs1Y!ᚲiJh۴S&\oNt.R&+WlI6a>"8.H2 9?)#Y\L5?`:]cjL/xm%) :,&O`+hVq)R†Yq9)$#)a Ml륤pgN 0%i>HvcQybkC|unJ3STcɫ 5 y qKbg1\Ec?$4r3hb]>p'1l#?#KBD_dQS Z>,iu+=U!A3?˿\qWJI(h ]Xj2C]%(c<#^!K`եpPhY5' I#53Z<A(6+"HA*p2K̪1NmSbޮ#BT%iFYfXU*iu1^yB(hI?LVg@|USHxS歯ʜ) "#LmzLK09@*soA', le=~ {J{Jh2s1t"pa/Y,cYiJ9VLkxYR^0av0G0Lv6NZ ^nna17Ibb+x 6g+JHh=順rEX]k}3h9:#i ^'^Or}REkU|'pN6}k:Gv%%"W(Q  sMd]yUNw[O:a8A31+"@IL![O3Rcu8:"鏰y^ÌaM-A?]Fc2"&7meUV[oЕ.Ehla_A *Y i>XM2{a[`y&gjC 6gb3>SL 4'Z-IvRO6ѓrFyڜ>Cl.b@a3yЦn.t]c4C0eiFd%|P_G ['tYI#ʮ [lزd'e(`V<0j݌DV>:&جҌpPvR`bܳTWt<_2Q63F.Ʀ!JhIETNjsN7TM^$p%.T{v]CiS\$!ݎ_L5 5>GI:VD0">nlƒA3mIՀn]Bj3Ydl&0$Y " zTmӪws%x/!ܹc”`]lwpy-/Ȱe-iT#zh Y##YF\#EzJ: [" W qBuni.r9 3Ħw:aE;U_dJ;a"bMe'73z,*]FTK”RC/ȰG҃+R3B0 Pxv RV >B * 3"8A:oR*\K ࢯL{bW#KWӠ+z{ ߳IMFR aW *RR3b/Wb/x#d]$X$"YD\IX E3q]emdDҲT ]!"g_Izf6b)+6>y[J%UktG Hp;WJ'EpHQno0oF'ccl!Jb2xUIe:d&mWJ+6n?dz eGI]r0vZ: (BXF0M$ݎvWO]3l/63$xRb0}$O*oK]`~a*iE\j5J5j 6 ڼUpTɓq%.(|qKۈ$V Lfa^S9-ݫT/˳\AqIE%+%zG`^b >i$6;X %ej 0|7s'(JwLQS գ'HsIQE2;F,o \j*c3D\-:Rg{c #/0\um(g ސuL:}ƒx5G0tx"b]|[aD>2#{M*h*Hs=,k(Vbe߂x"gt c1[7͔ɽj$Dh6vV 3?T(37e%M2PLy,$:Dw[M*~nt)K IY v98#eR*ڷkqfsuz5wt0<.F`Snқx]ˊ)SXȃCfRHz$T5$qDpV%hl닪-KсrYZhUr&:wr?/^gyړ=$YYI=שL|J(vOa2! _(?zJ( obO=~NV"X$ 3АZ.+땈$&:@ 9W۷x'X_sM$9O"0Ux6ab/ kɚD2$8l>ˠGW_ee[kzi15Pl \n#7I[d՛z@.@ \lg5薲w\`CMzy,14al_M6,{iDgwDjTn'a0NNfMt: Yov31h[w؎Y$!ۍ仗cK|EFWY9+7[5VN+ʥ><KJ SgB5A:8 2iw?T |plOϻׁb GrjR2rQѽY9kaa`S Qx(D#ז rg}T Ư[{Vfp6('r 2L4;VY[{oG3U #i +!JfL.,j%Vd@'>,>f%IP-6sT2.&"%0(gx\}q[lY@ !R÷R'+GGfla Tiy7 'sπ*g*'q^ G!FQG+rά3ܷ`n]ßa|rןdʯՂd?>\6gaj e6(qXH߇au$ wJwtc -P'A8.`G)۳xy  ,A2p7oxp_^o+7б Xßg_3#+e^57(B;uoQ)-V椖GVmԏX[z;8*2`|\tXoZp -ZW,D?KN:KB[r1ɣZ <'hHJSqil1 }7ȼ_pŘ {XH;&YHqGF5E`Gl{p'_0-0q.s{n`-ʽXKj1 Bt\YRK;`@\6 ,IR3gQyJ? AT^Mtۮ /I{$zI׆_UnĪNYrly2bHXWcH(EK}}R-C[Gs<_x#飫t#wם.Nmhh 6p'D|׊257n;%[YԸyc LsO^22r84Kuob$wBݻq@-ߕ˼eܫP#<\:TUH-'LrSt-;q*("SkX3!5*e) gMBhR =iiB x˞wOI ,X%15W1`YkykU4\_1uJD6,Ww{ĂZ;Eg3u#:h X Ěie܆/wسfC˟=ʹI7I 9Vtpm{ۿ=ӬK|GGEcic 4ABBdnxBK8l,j.Y%{. Z8NWK賺^;jCqbJN bb]\czAR-PtNϹ_s %b ]b /xQA!Zn#/Dؔ.(l2"JE8+TD T~w=me_/։ZF)KR%LhӖf[IT@%dWzPGq:+II?ZMhN:.-Eܸsa겗:09AM44BB# l:w.v4НTcG OeZVbD٭kp9 dMHj5"yjqz@%wI(7hy!"^جwz .cݹe&}c O9BOJ-z'xEƫ 3VfG9|m(! !&6wT " ~RɠM$͑@ ɬ@vj>`-6se)Z̼@Q9)s`fl["W#1}\|N5  d?+ =aRl'ӦٌgO7 R墆4p)Fq?dD0CIܴ7ȣ8]oR/ɕ0%osKGӱ1clBЄ<{1|B<i&ƕxw~zk*XP ^Y8> ͪzcD@dZԴCE(0h%ZU\kFKQï{ԌfCnksbNQ؍BШ^N3PY'857qw86߶3Fr T@R| Tyyޮ5b92&;o=o?n3%[DuT)rƷY| dO攳Umv ]k m'V}*Z~<#TV|1a\wӪz޻L-[Cu4 >/TZ/4f YҎi/|Wq^'QA6#}܍jsQimF8}l>4UiZ3nj.u){l+l$~ rz~B l>]U(ύXHi3qlӷWPQ8Vֆ䢵'qu6GMRtͺ2; nْTKܨjYF'͊rM?s7@sR!? ۝榮:hNO5ug =w֮([ql$s F1WK إb'KԑZ aLy}kp:iyAgWQ cDmI܆' V >0880)x/kVWK2? Rᆏ6.Yţ냧:NFLZWLiɥEu㍫Y:Z/W޻ UPRzF!s k#5s1 :Eճv;$<$D &IM`@9(b.[DmFz8YThtjpF搦 x=Vw k|irJVRNJOs(Dο|4tGƋP_>-Iq%d9JL#&āޭ.Oh2'ty2P5XGxN3'M]v[{֒{er7h4hs8K\>k%" 3Q3)kDWm#}>ϔ|+- V'OJ.h2g[1pE(lG쁅JM RQ XiƼDjIGz; 7D{puK>F $MHAӏe05])a2[qd6,nhCgw|"|rM sR7J\cj5@ $jp-uy-*gП&&@UĹM"_w3o2G&Aj,kڄe{UVjI T|;葕{Ҁ r]֓tZvBJKϽ ha)2l>I! (}t6bT}LFmg,S4|-hUKqq\bh 0G . OMȨ`/O/;5x>ϱqr Q6fgcY1Ge(QV~V \% L^k9VMZvin̯Ij,jI'ԸFnU@IzU9hNדg(klbφ:6*: S&.E1e6u'sIzpFsZvGQ:k%d\%>XO _nv$ksƈ P>`!1i7m.DξOfDŽ[2U@1rBk\<;9+[QіR Icu!ORuO0e]+xBxaeq*EB}|cX^2MK]P`i\9-ZAL q(_i &Si MܨU 5HD70*< w+ɋ6'}fWqf.mB!67̗_^f "ΨJhu3dV&C4O}؂^: 9r4@5zc=QhS?.nGd/у72%o2y׵tʗAc]͔W9ܟD ẂeRӑЮOS͉ڃJ2#YP :J]#MD;(.U=M cg R]pK`ds9ޠXţ0+J1¹8|/*f`E+ԥEWM偯 OU^*;\kzTxTt|F? e \2i7Vvb8Qr#֐A,HU] !e*ʲPv{kPt}`ܚ?3No$u*{f7>7 M|lL-/Islχy+}jqxVM bgJ4U\Z?@I=v}CcnMᒃ3}0OY|J]ZD_VsFjv4~y^d\2 ERtʳvڜA{删,Ŭi5o@C3!Hb笚>Ⅽkye:6I>u?'^'Q^enէæUBjakQ*ԱP{R͉<m]+%.e/VI%Je %kyh=QfK{5yX q{wJ`IwnnGJj\<9lS30y =C.ZiSMY)nLU n/弯yhdjR1S>6;M95]fPU3SkK:0>ׯYи+S>Jjlޒ|mam,A]EZ?uiISL%Iz꠺_`epbf[EtqzcMz9hE{eeu;}4(Qц.Z KZE Z@JW/J}/a(Vr3TM]s`x tASuVבF$M zN="4K(I_a6zїhGY0HݑyNDJb\b ܾjKe^/@Z~Q ҀqdT^W])vklZqʑQ9}PBK! 4r.ڙC y@Aޡg[E8ړA2aF{E˱LN+3gVMY8J 'NlR<Bܱpb?AXhnjF{Ϣv+h fpd(z_4*_,|¤/9-A,A x [́`Z+&aXxG䀉,-*6`b ,@Yve&>=K\,{a'XBpK) \"p'tWkOw ,A5'(XX&NXNtVu&owb?F-ՉR肒*,s ]+{܉eE#_v {ր1iIH n)nPmSWs \WIyЏY(6g~>|盥2(Gݿ,@.q - B7.9TgαXֺ Q!0 #ٱ=Gh!k M&KVsHԁOXT&U*exMγ,Q`m‘^LC1Y͡W⿮1[0}OnS&͂DcrʇJ՟(8s];kR,{`c7sbˮJB|7s{2ʿ8<)47v2;g1N$5Bg5k4|H*5x_>}4td=Ҁj6ODI:fbj?dibMjJ(AHeRqN r`69e\iLbxP#q53`"uӱɔܯƇQl!9vh J?JpF5 .Zcr$fQAa Hj ت;!jmڴr}H>0?$}/j%ѿ^?Gf'^Uc 7rk^V.3tx&f'c2t8i{ٳ?*Dl$ECKn9j$U|Ċ+[DB676fnza|:Ҿ_1ǕosRMEsj`>fڭzY#Z{\e)$̞/MYܧ(McŖ\dbG bY=y=AhG?jFE(p?kV` ԋN'Zp}޸! 0P Dvd@FeHEWafEg"sn5syWbSŝ O+)=oOkmM.$VYTQ2>o`([9kK9(tNq-Q,K~APCx#(2«\7ǐHhK޷IUa>mt Euގ E&U Cs/ LIdTm4&en~wKIjO1áV9S"-pruH,'Mv|:nrBݟ[!񦗾eao@1VoQ/ϼc"X2G9 #>!|p+IC#S]mT/ 5gM>Ý߯Bj5 ,&-m(e? ""lsjninU+hK" >ortG3$=:  ttS=W. +Ln(jAĬϡu ggMJ"Zղ Woʴ<-j{d [e_BQQI2OTdFB !EE,ׅz鿟RJůc B~2/-HŻm6O, o- JRݴQsSCa5<)R"('m[{]+D˙'V]([۞4kFgjuiJغII_M"~k6Nʔ*A :s3\w32_ J7Wgp]PPuޏf&C8ʽ QRcG[%!qbՙfq?,!/ zor޼ʜXxgYDE!Ȏ/7;!E-?uGE X6_i0W۾|vR4 mgĚƦa0 Ԗjfn7"%coxU|!lVk@z+Zνd!(1tQ v\1Ku}Irb*>R" o0Yةb.XubrMj9Nx(\)Rl 8mSW`}'fi[EPݱkK妅m!TnD:\vAa0zsnl(׽,0 J-_;,fgFBucC _g]#XCIЀU!4vk+~jU; BR?(*Wr+Ao |zOD5rꤟmx'u߀HKD͏JHvRC֕jqw3Iܨnr` A(DAw&76)z_N]k,pޞk!B7[FU|$ CgcC7 +R{^dt*î᳼q $bi9uJce69:e;* .\Ee+[VJ Q4Sw?MPhEZ㶭@ {S"!h=:BM+(9@>VVpò&{l{w*qsl~+SMIr}NfCdW{3gGU"`xA:1srt`L?{ˠCmCO\~%g_A%wq9O#VqHosVfhWhImʃY L<+8}&gE G!r6}c_A UëEs;Hnv^u(x5AD)ʢKF %oQN)ڇP+Ça t|J-A渢+uH҈MΣ_tpl>4C[1fkUƗ9>+3`tFk&}CCxw+ ۾V,y֮4hIiKBEc_}w7ĪȱISYOclξ]Z1j4MRЮ3Tw~z@}^o6d/lqP}٘mA;(f̙wKqٖTQ咊QBb&I4! 4Xlr.[u{޿ޤLDFFk kIdk}kN:]Ggi5qaXy%uct2%D~D*G7jrmTEfX׵ck"p@uI [W1fd kL[>>1phW INmFf%s&pFJ{X4O"-'B?C1=qq\2r@۱3Ƙ>VRf=,A%:֔/\X c(-<έ4¬*6i^iImQ{yh|Mb`k}ZM7/@pN}QFz GUwZ8\ߣ{;_|Uh—é<ionؕ l3v<<9 kV|:^oUfԷ]R#v ?p6Rf([ErS-T͈9#:(GZN:B_׻?~RtkLze"k"w6!a"/ ;~kfh=vM5.pes)O6_P$ -ֵ W #,]^5D<~,fEAl\%r"6[ķ]ʬQ gn0Оr WΫC0:vV-zuDfj2T0&^î8gY x.BeMyQ\MSì6 gI˹tD~<]:a wޏ1ڞdsόhf¦feݓĴk}j4&?jK){Л6/U߄[s<_k8s߄tZ1žkR%3^|fFN%d~f讧 '溎f9dAf-hH+vhƔGm[q[o绝՞]Z.'Np4ˌ6ݭ&}xo^BE!RtrDJȀ`jSh}Ňy S b,@EfHR)^5dˀkN!* 5ฑ0sNd:B`L4F(l<ޟP)h5fӡ88ko&V.AXw]fHƇ.҂p4I ELNI֛s((-I8I#j9W6< zˉX S|y[ ):if˗2Cὓ 7/4ǵDW27|>By&=T_lc+j8[}‰  1`H+>OU$t?e?! "?駖z{Ǎ Xü|[|̖:QלskUj.UeS$ KyQ8ZNFj̛smu= M͘6Ski4vbtK+j%7{ $6z$>Dw9eoQU涴cZn",3Ozbn 6%  X-*8$'hcSH΋rn1 `P&#)iBc7> u3oc-0){|CCko'BG$W\wRfG"&EP/ϸoe!Lb&vDjLe (o`71֋}W]}3,~%qА)##fZh^g.{}h֣ o}.!_`)#E5.lr..K4I Z}蛓E?9)Kh rzE岧E RCxf2fU:E\oIwJc&/'t k-$SsEҹl' OrWGih`vtsRɨTwNaRCkcL^ ڇK6]㚵%hrosd0@!,z6)=3A/@k%ֱA ?U$bg$]3'GSHhIb 2twCeB?G qOP,' qIvlЧ N#yQyԩ)~Vw8sqCsޢqJ2g_^5LIU @,u7bsp!ȺN/ЀTB[" = 9y~ }J' Li;mNN(a@Xh 5 |'jU{^BU|ݨlVc`L8ԅֻmT ֜0Z+<~)U6JyyX6%3})7R 2s#2E*H95%Ԥ7RiD*Lyh*a.ygm{j^쌟6YZ-P ްus69lr9iq:'%k&$> |c e(dfa6$eQZsd$vn(5Hnn _AyTa&H0(.axLщ&ta\ ,k`VԨߪچG wiRRdDȰ@)N1H;m5Sq$ġfvuZi0@Flaxzq8}#]/#R{!7}ҼDIMf Fw"$6[MѠ]r,k FjDӯRd4ck6fY~`c'L:b@v*iƕ}#fن=z][ddKM)٦eF[F.i2\BE!RZ2ٜI/ Whjޮ3?bJFĤ3s!f_IҜl"k~YMд'k&;N["vgɔ11cj7P4SbSY6N%zq-x ",wP!zh^ݣ H(̃CźP\ ^! VHY9a)I*\ +gVtfrPv^"[NqgW˄iv/7~! H)dkL8CnھXH3rG-r3su+Y)bRl]jikgq&b=X\YC({aSjH;AҺ_PLХ 8"ٴN|=R̓t3.m=s޺.J l4:|n"l^$|i-r+Zl=xᔂݐwWh`퇗&$(`/793k_l;'Лm f 9^K@wۛOnY(`oVA[ѽSn Q,%i'rG,"6X&|ҤA;l!y&l .>MkWY]5c  Fzfug׳r>70?1CV~> x=m;{N }}Z֛ZoW?^_h[dI2M4" ǐCY B.-w1U9ێtDkW|VAu!"HqKE6Lz :ۖ@F ҜxEm^~JCdJmͰYe[?.JϷB[IAMxFTȲ j""!q#A M!Nk 0}^bE@fG#r K7rtYBS\6H";zWcʰwO^yi,=\s,/ݽ)) րuA am$GN7GIr N8F4ؖFP2L@OFr9dP#e1=oN(+q+2PɻB6J Rde5v'DZآכ##"q"O`vNڊ;0<4s곓#"'bIb4 K)D>𘎨MؤCh-0W;A{bm]\Ȝ:>2{CEp9>8=ԙF:Q>7F=z(Xt[I7 ST$ܥ6B';M,Ay"/ Gian:|  T߸2s<# %hRK>LJ_|_kO*b2khugcf*EΦs_'o\zVtS3q$EØv6! QQcUYk6rʛh^nQ-I"#^u$*.QDQY3n~ENu!܈ԴB%1$1`6-Iwd4X6XS$Zh**kUf8$lMmvjaR1nƙNNBP^OC3S |Z.: 9[TvLEQñҴ!& :v`}$Jm y{fZ/2iPP_PC7Iʽ/YwˋH$.UOy15]fRTEڏ6o_/YS4RG׀+-iBpD(gCF.?i!ʪ@E4Bڧ`btN_BTpCӌ!ji.7 ENXb1X-WS+KtF">]g6d#h=ѐ8!A$t #O B҉f^{>I&RoJd* DwM!)zlh1bSp6ZeFae4R٬70{.dtQ~zPRԯ4C tϺr- =_#.06mzԜ<:~S1zt"ͼ{bF=뿊^}^ۤtDLs(G2gZ8 f8pE~D:+Z{M׳-mߤF柺`NxaP+q~mq13GС=:.z|M#5`Mh`@C/!^suDLȻJiV3u3{ϖp. CtyQi\/+IQ *hwi.5s(A}oKisgRg…)Pa@hr}o3[x<k# PEt  _zp mD0/Oi@զwK{XA#keoyzBG.FܠP0֡BcmMo|\~0h)i}wZHa> I YFѫbFo=dGXIH;l軰(2tj.! kվ_榷wH}aJ{vZ2|x(B,Gk\麷j3&4 2T!ff2k!ŴA$f5RgC,9VR] b# z,0_0ԒbcmkءY=K7̸208MgbIv' wcmTZ(Z ڄSr''s? :Fkv>J{$H_8DM^&6 !3P[O|l>KeLj_L8q&!N;|ۻCCx*F7i˛o~,FwDKm]2d3nG\B>-ݚX7 8W޼+eٜ(/0Te@e4 bq vlH AdT_K~w >%kM4{Q-hfj=q`t%UITQkMɋ9-3DU.!ݣX}GێÓZC#wӄ1)٘!Zm MIՑ/7u1L坙^^:7%$^럝d$rrpG&  f H? Ԟw)5o%oC6SzBJ?PR=%D^"{+9g$-ގ Zwߛlщ_[EAI(f$ީ|!lR}; Wh1"$\<@Ym+GYL}ZFd3O̱Po;9CQ2Ub830nm,O> R**%E2+q,X7;6PZ Pie8ƚ' 37 =o wpϠie?'ODRuJi~c@@3wwy?Y">dB:8D0HP087*7y(\rг }}puI[o=뷅c}svm*8Ug\BL|h[s:ޱEӣ@рhZD6d&<E->p DZ%CV4=Cb1Fan;$@NgA#h؅CLX29$& S _XFT8Sqin W<ܕ"U<' DF++9wz8Xele{:faJ#4QL gδz "︙S<"0j!؞AkR4z]‘v(;~܊@^6bh8"EIF}=mn`'!o,&6`1"X4Ѫ&FVCpWyWkܣn=Tj[)! :6P켄ȋB${nuN,FtgNZhOF8hR ]Txb̈́l_Y}_Li.=ai~(4a1}jXb8 ~R,r sA#Oq*;P:]Ec&$TarXڎiHsVd]М *9?0ZDIqtxvm^ʆVJ8i3TLzT!L[\cc3 \XTw.LLLF({K*8di$ FԔ+NL鿇y8=(%d\"X)XNgsm![VPja&%C[ThԢ 2j>~"hPr}.bh] ~Pe8[a ,i>LFwaA3C9EЉ]lt}xˆ|l")vyӁHTpŧď4*gD*ԟ/hBe$ݎp*+V#)O!n?3YrK(D'NzyV,MBMz!%)=7^nzպ"7>_d*Jɂ3mLpbH×]:&"3>ˮĤ F`ei^cZ;^#LjhfB*mE {}CVR[1cIq=7I"N-ˆI8Ljjڔxp%0>AԇKfFȲ6ƮwcFN`LF ]K= c',WfP܊њ\I:C@SU5=Mu)YOٓ݉ ҷ%68’B-S0+Sjo2FǮm/S:iZ@RI9EFHW)\BE!X 1f*B[L֔^2֝ ;\Cj e/xN T#,Kkja5Pp [f1GAkКRwq f*(5#aP5U"V[ Vu%M*,DMudMŀ-N>&W@&TVC,K3Q8@ QDhV-Y>Ǡ?$JhT`~@6* PR֩@ZK݀]$eZKrO00d$uƱkx^Ӝv6%3q#PSDI^8ب&<=r}F͌!VUq=͔#pȸ!KHkdsw˷aP祉ߋɦo?.!"#4cLU7%Mo4Wuև;@8h.=':v*CЄ^MaFǔb0EFRof C-)T+ot;UiD@D93H&P|i 2ϒ=ɬ|vhREA…Bm]Ŋ'eA ơ (m ' J\Õ)&4.rAX )~H:׺ XO-H.8YKCN7 !½C@&IMPvGf[y#d)[L4Q(ai|r`.۾SxxQ tX+#Bv$͵92*lHMzGlTu $M~JԌbr2?PA.:Yl͉ kI$jtCch0z`TtF]4}&D4fb~I8|o>EI<5ibȉ#u!qkůT6m괻8qDA>h)}4sjCpr'PBNXIOS?'-&3(j` ٠sa+MH lJLBҁ u\Lj!y4!9OOle%ŧǮG#D2h! aF*K֯8a־&/!iRϱ vGp!8s hlhws pm8ƮpM--Ekx=r%84F Y5g-S='ɤ_L JNՉbGbF"2amt 0YHJ3C3g"3h"C$Z& ؤz]:HtSg}pGtXa4FsPjy#kaB"L4]a5FĺACsŒ g>:9HpN]Zr+ 7OJM3e!K3b{|FZ -v0UxRqpJRys?J 95VhaAag*GL\М$Zt`j~NM_q Kx-&|}[<3ʑid/u*()Qa@6ONG>Zp?>Ƨ'0B!Z?s/s>}Ei fLօltӬ RNر&h64ٳ*1bX6%/ v$Eޖ\'Eɜqb"'x4mR#(aJHx3k3WoQ\¦\vT `eCH6/U-7P⿻a9&-܏w;]VmޤLss/ԁ3ȞNA ޷`휹r{Q`O?^Md 4KQE t?R[b+M*Y#MF׫aEDF%D^"5OQhCr`eGR.f =ڈbFi6#Odw;.ܟs3wqh0E C?Yy1fkl{v" ?u=+T[XDi[5l{i` PKjԞ);emZV#r{`'nG}3]vճ,+r]*˂G[/_,Za#ais?}[}Ԏg!&<% m_n30'P\RWHH$-*4^QEϱ tt@S> zznڞך^#n2p9RX0a4!1iRϗyQy U?2Ypk#S1Q(.z9N^9 h130П//tw@rfk㿿gw&;>5oO 7n5{{;i `|3p#  o* ?.aP $wQqF$h|*!> XP`H%Vpk1i vLM1`й?=oo3:ǻm,7~s _ЃQPRO!IhJ 6)K: b9t tbf15iF'c+(tac hz &5 0=1yF7ɗ}8!E2STឨUHb@+N]e0,ӠSyfb́k;v2VƏo+MKUb<u^@qpLJ8sH?q-Wٮvu 6V,wȾ! =ҔK^!23u~6a~@l0 Ά~:M mTsjkZHW,ըyTfB*9 % VM.7y@Aw+6?'l"(F|SO$u$%,})c q , oTsaƐ+Gf?, ,R8o$kCD[lz9Dϖ}`QB6iٴzRjtJu+b@yå_|å-_p_???wG#_|{K[fo-g-qwurrsV z-ǶO꼩?iWu-O_^ kgß<D]|w Rot$['_v♠g8i<&/!˦kvC:J!;,Xp[ju,gbLЬV5l>dnf ըl@7J072+[kgm*QI`uU|d T4ʈN@t%9cEY2B!rzⲴ9JAgx[A3F?-4pև+`O %yEhx?cÅW{Ջw5{vXj}?k?,TvOG=1g; ?@ڱ'|4SV{uo!XS !Q7 9?s8{2sbOy"o'`ocZtyX:|ձ[6#*dr<5 RU•Č+Иw 2 y&YͶ2VjJpssŎP+&K]ܳ%zV9ɵG#͖lZrP= $Ş~S]n?<&9h :d=~t[g\$W\ii)v^^ Qm ȸtxAkXvR!pGu y9Q"мQMXy*ȣno6ڵК\q@&pC&(9{Ӌ ?P kQ&N)Q(0PXvLL d;;D &G匣QRU6@ |6[%k7ۋޙ~5n[h4"(&&,O3w;{JE?3FFFlx^\sqwgRPx0t^'?\= wʦ:|0%)6۵zzp]Dp8}䱅p̬%5g Zwf@ 2Vnu C9)- 3D4eSN'bGhCc@8h͜bָC "K StK`_>shM>e/1` 8#I#IT8κL)?C)(M.;w2DȣWQ9ȥ%CU 7q8[sj$%aK[-f8>PRk;)?FrsFf]ht!YIxHXPEXq'G<UCEF,E[:>bba@5]s=;7uCIN@ƸuZj;[# 85(Skw܇x|&YꪒqCukbnQtapsycJ)ЍFMi8@rq)O}92_IX+|̲EJasLNuBA[ˆ IP=[b"̣zesdvT'ߞAdBqK B]l( <7#=)X0UB^_uMo#$!k d%ʭ(dZlY*hyȵE8/rv=QLm_mHCЛ . O2NztJ[ t"1ɦܑ"COa4hL ;فhp¹tC |l-(Dͩ&w's˦8GrDA}Ҍ(i\L˅>?Y/?5AsH Y/ CIBq-BBH  ".")JV,1W ~U=KX*fZBv0A`D;1~rlI^~ZF"QfT))D>ޜf%J>AegJ.]Q? Gc˃t?:1-Haf$:G XwL1.o 9:i:FpEa[& y3, 4=^u '˦L,{\G]Rr\_0͊tӞpM~spm3[pf@.Ic"ꭶ "6nF-lal;3b)H"nJPϨ"Bcm# Sr鿟J-kt:Ӳ`X\8Y )rQ>E9i>L5+A)nX<JgChp !NG>qv =U > 4jǚǕ 5ʟOR-^vw(ChDeֽBK]D~*D2wK:/v*X$jv—ދ$Za BNH*A+)KG)l(PBxFIn:]7|i K̸< Á86fɜn;Hʂy_F Ȧ$K%{V4Qv碗kuL$wC!RI cu'k4gѫe(ncҢcO%HVۋ(xc =ox o_c5*@$%E=SSM| 寏jocADۖyUETx:Qaɍo׳psk [q+Y^UkϝDq P=5ܱn 7-%NovV`cVb7AB$>%hQQa˘aTq[W6dĬ^vP| 5TLüłĬxH!$]YSq"4iRjْ#EzoY `ѓKe2h8-n'.ICm7~!ܒ)rwhOeJ!>Q^l=!o* Z~[&i./wI9t r[h0{h)}gO9_1b(^#6s,R P=7gž5O>s5_t(x-XW- \AܷR?R|Yvd=!pZlkFdn8ɼH渑@(ʽt 0 k [3TdJ +Io.)@Rfjk>3_Mn©LV)No֕JX7J!j[3G"&ZH&!FǢJ6pCe,1}'jԦC 01#k@' sg&zh@2471V% W:Fe0qC BPX@al[ DHHH=:f(, mq6bÃʆm[ɻP@^bu-[atIMQr{0v;:`O".x w:6vט,x7I "Ib\bPQ.(i `|ˀOYZŖJ$>wf*唂Yw.ok?)ڄ^mcд!A($r#WuC "?9t(ɣʈ%<\Лz< DqCY7d Es!M~CLzJG .\C0$lc %YH^D9D9EIj+q e8 =_thОLFғ13 E%7Φ/;fespϵU9>2vźNAM23 Dv/vnWn[TY )CA䷕DV 4r#ޟ[-1W@. [}c22 -CC 0|/z ]?BꡂGh )Jq`_Eu3㎵Q] ۜ+T:^n&fI$&4z6);%>'jrސԣE _tIcD f(׶c "Ƹ4 z"3?Vr6:D 4d}hB=#Xk[z4rŭT)Q|`AۃJ]udl'jj΂FYsytrsC*FGtypW1|R<>OȐC/GbĢ'cCyƙjz S˺/rB!J0W&s_hJEސ wAH P47kf򈅡Lv}ƽB9P.λT IpHFt gX#ݱG{.NDd!"J1pY˖ٙOIw8۠ȟ^|"= bq"߷~b40i)J9ҙn:Y>Y,qFtsX3 (ַķw(Ř߬ v7"&=4hA¾]!$:DDY.v4jȚ&׬.'l|` ΐaD} Da/%{qz.casuao5ivGS쨬 I 3|BR^MQdPfz+FF6e(,ug9X@<nL,84@> Rd B,^$`CXCLPϸvpqJN5 OMdWرފqёlu2Ya i1bFΈ"gG~H?_$ge%FjTF3!9uy}62ؼsgְ#i"םUɧ "IA0#t:hQ?KEolD2{z~ځ{Vmj dJ<^ueq-m1Y5D@6hQ")I;0=OH.i6}13EdT&r'RXi*HM[ ҴsN7e0.Y( 3]!fIj{r!9 2ŞĞ&&۩zC,ˬ v3ct[q똌^r5o/81R4~h-OI: DՒ?~8,M3!ޘ@*<,tVsB+_簞 SWE-|b / jn4}H\*d8D6bRpnw-Wȅ! |V}R{ZT}(gyEe(Jgk!FE7buo|SRwx4fD)bL9IgY70kO_m$Zi K%f90*X]MV'za ] ]kVy\ڮԧtdp79(LrHىa# B'|i\gqRaX!=+E[xm [nc^%5[f>=9N8m73Hzc\=>fltwB? '?#ȗw6y0n]rV->ؑ~KnZ$Rl3 -gP5OVNgi}ڴ ~e>Tҝ|kY(6aQQxxS;{6o% A:f ^||g`o/Z?˂qz;٧^ǿɦ_OcF !拐U:Nނf z[[;wti>>'t̞vil!? o+B-(ޛ7CQ;/~}*Iyk';H>/A~77v),~˟l3Q|6l ${:I%7eAgcC;<1B\P!VƨQyZɒ\%WᑹT >=Z %mzb#NOGz: 2b_Fu'w+ǵmPb.[$4 ~NWh,u0 "?"n#eo/ݲ#hqI ({1$ug] nz6ܠC;"ڢ>j!aǨo,[Mؔn`0P&! #yTFV=ſП ڑڴ Bι,YMr˅(RTr,se0s.w(m`Ru FKܩ0tuǜryO aGflZZ p!&ued[`0̮&(;n[6^֗NozߧyR~]1rl2T$_Y)w-\9fQ}41cP!6\03L O!x.:V|2}wsJ؄%rD-wrIy+zId@ "?cQTrimY( mBLw҆gw_;QnwoAhҤ))c}y1 Shb3 o}.;?2fv|R (Д}9@TF e0+[41rizhIBƁ(s@$EƢ4J{=:O#I6KYFE*4>7U$*|0Vyׂb)uL H;!u&%wE|snhõQ)"M, >mA̯U;ʈ}f~OAۿlXXCS>}2=̞i (g=Z&MmuVg)|2:BA ;{2Sc c!m<6 J|zLL-C"}@dP)a}F|aɩ<ތHs NCȧ ]vhEcJ5aca}lzv`T\GNٙ νt'w/}PW]:gA2ӥ*~LiF[Q$ r|(DB<cTx"=HikYE׾͉+~SA]k .5:HA$y4 (:ϱ +Tx٥+ N=PrCm{ HGXVxo/c˵$G[Դ0{rhң ϏgV_/*:2Zbh>ڄ8}2%yJ[9nt }yfV6,Ґ scNp亳< Nu"9$$S 2lAE}n n{d !VEWՇ03Wܧ90d܋~A]k3acSugHqw۰bRW}A5qp{2ӆ>R/C{:&z;Bӡ]?ҏ#6Fu2gOqn%)gC_kod|릛 9:6Z> {lN >} n3HH׎ /D~tKhlIz Z3F$ >RKC+ w2%- Z3AӦP]vJk[,ƴaVfqgVaBZu | V &_b敕 2e2K;5r23Q|$"t>حoJ!xnmnvNι. O|"| kFaeEUtC2$-WUK+bR mmN)FS7NB#_ntX<7kjo{O>-Ud+Îy r۞`Eh@Rx'4h)0wU|~a FTm"G[v`:= [|نc ]gqם[o MלZ8Il\GSgӺVFcza9 #M^kW*i7`G=x8?J\9D%=Ëܷ١#vbF|ABdC'B^V/ GDMV/ZhL(Hel H(ȭ^Co$+[o-ۅ>t.JkJeN+i, >_X6]ܯ5.N]@ptDȲ%+ދA0x8,} ]\oˏ Lik@\( Cـ@5tÂ8NRIҏwRIo@򞥁Vz3D]H9 7=P#ޓ!EwlJoс!$1:HVvVvmx69}0оs%s {2'DC,w $S7)F'y]8&5qdCߢaA]RBa#gubNOWG1=xqCkuظJU(g^k^B6jXZ{2ֲ’ؚc3,#n3kttXn{"FcJi>n-Tp;T'—(wv,Z;s/Kʇр)آ/mdNu $?V:X$\qz9UNJɄ/% Ok }dVJWP&qj2Jf1"rlpH7N $0 ܚz*r 1?$!BFŹߙzIIQo=!'?P0 d{&Ŷas>Q'7U.l "Yxd:K U짛څHHmsC7YSt|3+ӠCj&Sjn{֥!S9uHGrhyU\g7LY8B%}R4Omf5ҍ\()%oH*s\OI3g~ HĭF-4^tw iaW eƗ()Rt&M^681J9'mu!7/uu24Z0=&CiD$mC'RV3a2k72SXA2#Ětyrp?:ߏ\yVQGW T{ag6$FM6 W|$|8|!:N4!lz~dH%ڞ8N2Bs|r ύO~jV}_u7"yD%8o_}}2"*.Ԛ3?-[ `dq%^_zU9kqfIɢB )%%āKX34DD@v=07%Π.Ͱ:ZަG2@ 1TT &,d䴳СŒ8.} fA j'Yɛ݈qKO(=7n o`钞 eMm'>f`Q!3LׄUeoyfC#rZXkpoM $O6KCAG R-A = eB:8^(|dDth6-w^=@o7a`[|ʹRM-r# @'3s]X[w{ S!E(ECOȯo<oYDieJF#89NZE.xA.;s1p Y=_6ԡ`[4&Q& Eaǃ~НHCb#mc8毹 $Κw=`,z;4%LhpFzdlB3RԐ#-wdɕ)! r>x\I :B$|=}9ľ&?Qv1_ 2ޞfN7s92œN'>U."3/B#QB kmؘN qy}w'axXSwh1Sa]s^17g ȀHLr"+@fjӽB>Bn‰93R!QLgw ty w 1ua[DUDLyR1N.u1ؘ/ ̚I?w_{QXY2{{"UH,fPGH>:ڌӅe ˏ6 Feiq 3IlbR$_~$Vn0(hz%->ŴXt8FG6l[>Оt*3txKq!Ҝw} PI.|O4^V.1/C  NMм۔GlDVȨΜCV'~7m.8}XNfȣ5uCm7̂#Yͭhڄ _N{3hDt-3dc@&5UskZ>mZ X,tw\hCbb%fw7ʎɔ ^$%`({z5.RdrE~:Db\# z9x H?Z_~1 /j˼[Aƀ6aЦU}X/(['#.ZSeGJb0{Lf&i3hp)h;ir^dmBġytC65(Fio% dS¨Xv 䞫AT ^$r DXr uDHtQ|t瑤h;cKSߢJ7Kh`il\t&ؠn9{wG/:Ƣs]+nu}b2fx:4=$ٜ(F{l K Y#\OET 'LpcZqwōX ^!HeRޏ "Qg\p7km&}!Ȓ1h6a {VqZF%/'۳>RP.DBK2i]$OH[¿e|D{qFF8\N.b,\__]~ MW&ӦO Ƀ_Փd?h?o^<1mSI_5gmԏO˷!A5=xg}~4ǂI>(6i.Àgx~ Wv江,ҧWx*`<li1oOC x]shuq|]j]{l!t]]?#g$] t3Ybp=󞝀{#W '.'{KShݒdr-sa@C<=/(Jkro]V$#UWz)|g&|^zr1 Fo'F_ro0$񵘆m2R{gQeos0t~!3T}TJFƮJnj4 'FFM3πȆ9M؈u9~~?}Wƛ'nˋvʬ-v-zӳ+~~3ۀ<9+$e>~g~T9$ɤY`fhW{m "~88_}xwB8O-. odyQ`Ϡgl$+Z9/v֥-zi냲U|43xv,8I 'kM;pzx>i]W5& cUpdl'!CGnRlrIȭKIg3"}ñ9^`=9yQrOqxVa"pޟۙS.?G{ 91C[k$xsgG! uG߻ O%t[g3|o8=!JthqUmahӚq)i:Op3Мl*T~=|5~#+g%dto 4=h'/Ycθn4reíGI=C'iYQM,>3ANK D)-iv4 j~k>,| DqS7"]{f$:anvNݮoڋBx(^K_&YYr2N_rPEsɚ3WUŭBX\6MIerNj{#5;[m8#?₍{Zڛ &ro%9L\3JIIs:t07"jueuY S6ODbcCqܖ$>{U`nl͝"Y6jI+,1ITә!W=D)\=k|:DΛ>+&+4G{q'kF{SY㙡 k:56sAɆ(|dS7ھ6+]]%Ĭ4mq€ݜ쥀A%#oYެUjG\{d^uC!b 4dڶX7Ԟv;;9o@2-0}[6wfFbwg6T`gV_[ڸӒ`gt$d2ٺY$l]ƒvdNxœ$&W* 6B6Քh)$㗞 /ϭ[*J?Tp\qbOfVL&801u*FKu`l=:_&D" ?i~T90K݈.|u__Se;y7(dO{HCK޾fӊšSvNͥ#CR,6^09:rѥi@WM{3c2gM unlht#lph׭¨%9 g c &Ђw3vwwM[D,ePxe X.6xÖ卵 _UOw2=;zfD_;{?{1ώPDJR̓w9tY,:~vQܰrq1V"˸7>]xîX81z#\K"G4&~X2`WưV!BAD4`U6 BƂp~PEu>J(`F TǠaX>=9%ڭūY G,(%ŹJg:3~ЬYxR]B*l5Jnp0ь G\F$* 7.6e4>[Bj5d?:"εON/Rdf"y=lV sl& a立v2hPPYÑ 1xDZOά CtiQj=:mduHcrv6p'g-`"j ѨzD%K R ڄ"C)%_' a0ͭ̍,D V4Hj~K*],q@ˮߌ-q(2ptgIC%=/wlT>y3U֌b3B7۩]:DL!j -m>Mjgsqe{rՒspKk& >ΘQO?1ˆ>W1/)!e,Zg+RBG⊟ώf1 i8n'h/ReŁEHNd̐$$t |]<3BCEH:dȨ KYE#f,+M֌eB\p|TB%R@U -wUm[q &Y=n\=yFͨ Y6h7BOv[΃+$0:qCVJ,9MɉVC~g;P2_&FK>=mU7iT!>~>:#܁JYIy^q[ !b%Y)vO`@EN yoRx a.#uWX0y UF 滲X l.OG}é@Z 'w/cunOnP)a+__M vyi1#X3RT-'|"Sv`=Yv%4"W}|RpiyFH4 cG8-HL^S3u&DA98qĽT_TԆݪX!,ao^Cew7lK{^ć#E\a<]#$G:K+Y-#KUs92نO7 ň4E>F{@YT-X,LЦ" U\TzBvFH Cmj]peIT7SM1tM X8wHb:NQ죵Gvfhahb;sR-ttޜhgNo:7DXbP#%#c`oA6l]r2d,DfV͆!V}t#(0dh˺|Na fg|?9>}vxʏ-O!癜>71\"(|eS"|;d&ȝp;ưKKrDt ]gG#?sdV` o9.beHA`咛Mwe6cwe\Q_<7Sg"2!@9Z\G{pJE6<g6 u! ڸa d3+8>:rȧke-hCO~ ѮAm lp*E_ VMK_ =(NC݋5 aZ'Y@Ep–^"AFIA(t{99eK1 Đ vkxF- %}e>Иodh7F]H7AdBD.bkmQZxPkbC*1EԄQs=C a g1$rs.{Ӻޞ~=csi=_ݛi7=zh_Z5=[ywZتSj#B4*Dڶ #.-ooﳕHB@'ٙDJ{W+>ڞLW=_Ǘ[ioٷ7%6}m ׮ZFK[qoi<@C9LRT O U,''|0qq52RM08Bf0NB QȘ [&@?מ5MsWL" VT-4-f8!#HUp]|02#'(e76z֛ݤ;9\ Jf[p#R`2P]fLZ_}1&xU~Y7cGKOnh#VzheW/Y̲WFWY;Y&e#ƁTjvIoڎ{q`q[#Zxxjzh5Ұ^|u>K vׂ?]NOy..bhc;pf˼kFq٫\`vCӤ!]!Bt-)rw6>Y`ыnZ>'Zn>O1]n ]]o){u 4 )GrCW?h%] ti󞕟"$5FHq+#uI̦$xDlmnY<ٝˍSnۊMàD(R"HkŞHԗ'+l%% y`jvԇM9iy3 :@ -,ӵ}8y! )^=>9QM"Yxn,mpwgVM1ć,>L&mϻ ZD֌Kj'l Z_LL+kYiHLRT7C:QNl _;OQHFbe:vq ApC;:9f>ؚ(RY,N%JNxlqg&:IǦ;(VZ$)nLRuQ0z~q9ЭB%$$# }2 ogpCH#qGs 4iD]rU]HK*m \dhbn 6^ m+t5tˮxD H룥 ne QS۰9hᖁN< 2(%(ujB+~a7GdQ16^~Pvw㖭^j;dQ92fd8n:t!,YD ڠ ̟ׄz'ۼtoHL']3j@=t5aMiSm&\kzX&6tr8h'l/ }pz"1߃YD Si.!oMK+O-el Ёi\ n΀# )Ujlh+KJsfL eP2tń7 &sƽ\G8+f:wn)?л \|a:$cA㨖)Y(P_!ZRjྲ;7cs""l~$HgDLL$gRn@JȬ8\C޹u44h -,Dd%v[ a2xݶIu LBڦ}8TImhfw݋SO1,{:֌$jly}4 _"@1q.Q3OG.ceRȨL+`#mFjĻ&ehc.uwP tcH*V *Ӧ7*g[EE$}*cbHkMY!b&]^ǴѤCd㉧oNݶ# 3nw2bYEnT'N1\îyޢ(dF{0]8;tK*ŋgv Yd3JaI+ZܷF.I[3ߪeHB<, L6Ur#BO &YINp"&oBePkgj!ꃃPjL}(v6L Bb+gw-=ЛзjңN f4ph:Pjf9fGu\EKmSx#'BܡԎu^7c|(҈y.rsGLr@ Ocֱ$.@|7^dɯsduMΚ.>FjNF~!˵:TJdo8PBΒW<)'vd#_,͉[zΆs,]bԇ^r1VAJ3 :Kp-zڇ߈'є}~Lg<0$6@v>yzYk~rO @II>؅2?_i)@Fʁ uZ-NYO(bò`rrv^mu~PBrS,,s' 5 GJ=E"ϣA6M_pF: n*]|D fM[_[!'Ȩ#*6ovTGe,ޯMl!}TY܅\6BF\΍'a\NA פ:)Mtwr.#X=c(fog#;r7\aS˜J<_p2ܨi41{CX7E|\J#|Tug7 rh9Rh&MW} <ʪSr- Ps `-CG;װ QNĭj$?(1Whֵ홁gF2Y(I"@ #slY^eh4X+$:㞢C4قb!`?.: WN2%~$_bh$AMRXw#q=)Փ䪨B%45^& QilAo(j.* ЖZAG|妛$14'1DGi% d9bݸ9Z,(23-̔^/.&Vǩ1ڝP:97Ѫe5qˁ: BXf_6Z xl W{m%&KfpU[r3g‡i.\<%1#ݹ1>T$It< N˰{3]{0{E@.' r[S`ܪ_ic ,iW+nG,d3b,7ǡ(/?z6f"G* _!I(N-a.1>Kj,,ztσj2EyTA:eU7@aN +ա=Х""urP̱֕xښG= nU]Od|= ڸ~evuaykAO Y<vf `kapaho!94l wC|rnH*3I g/ Ӛ͹KkSڬ6} '& 5ͤ=Ӄ?__+l3^|L0ӂ+caKʠep(ݴ7ـI^ & XA{2f aܰaBvYBi#pO|lMFA ucܹ5Pi*d &-zi`Q GP44u0 Ĥ`MNj04iu'FK$7e|zP+? I=G̚Ԑ꣉gu6 /Im!(o~ ZMO (;7꽄Iܮjh^.>5Ah) WӂdHCf0ۇ/d?oa㻇^㾹_=e-c^,=_}菏^}s6+3T2^X '_qL/hԳVvYx|K!$軧K`)'i9cx?_>uD.-mωK/lg|ycA?TB:Z}X; 쓝/An)3j0QG &!UHC7IbNŐDMҰq h$jÅ եHϢq5uټ}(5PQPjeBJD^"}15A @+׹i7 ljA"TiLJ^PÊIF1D+D:+qBXWW .1LWY1#%;D:VgISIu&iUY!TQvTHim$rT+K43y1s$4ȀYHE㘏6\,2&K[f'R`z3GTH+2,6J ,aȤM[(dxN_mI_cJ`3j)\b=0 ;ZsdʲHT/"^[eX),Y[2хH PV#}z!c?ѩQ* j ,+Y?4<,ekN|55ԠBKS ; O1s :|"PLDBȘ$`_uFK { 1rv_u'b rc (vI6Yƃ2bjC]đBP4  !s0p.ˑ%'ȴ\r^NĀn ɠC ~nc=mak>-p([3zN0s6Cz2_9D̈7+}<P&34KK9q9 V]4wPJZ4[->'H,ľ;Dc:XfZ:`*^ 6ۉ OɉҚÿhi8AGi> u0c䒵뗡+So^5g=ܜTu$ʉ9ui/Rణvk6{ĵ$`GfgC3,z-"4s }28CLOf =. q_.EP#/ NM礂Sݺ?MpЮY6-8\4v@m)My'Ƹlse2֛pA1JiU1ۻcfZy2P>>v/Y̼8Phʳf:^ʲ^.IV߲k(CA$'}ب(Vi.G8,A/륝7 #z죑"AiX Эs̔h1|_']'|nGq d;q>fK톆OUgrP #&0ښ^1y:zk_4'pwv[R% |NL>CMW+yEh Od9hgOz' 'C 2˹sbl/0sY`E,=Z堒  Č44cz ])2!z C(7-QM_]0|!fZy칡y7veuCdC<Ԇ d2ONlcHrQtdƦ5ia3*\>Q-y,^ ~3Qt=)aհ>,\rY\'Omxf4pȎ:c퓬lb6'V6O&>2P sp'm0QaDi"2aP)yPV.M)"ӋyR3x9r X\]D:chFHZK&Ѝyx͢O=Ԝ$&73ld0F!"Wi _BsnɎ!P̦eʴ}iR d"X72a5Z3頦S NEV7%]#6瑶}P<,=mqƱCB蚊~8;FHrju(g2^D7':%6&ϑs3&u\?L?" Ԯؽ[%!V<.oDl᥿1~KgOcK2vӠ1`Hfv#zZ#J!#rcCa9mAޢ3Z_OA5OqбֲHtMh3q/rw%T{!6θO !⯈Gp1!ltlj兜[L:]q˜C :0D`H@j(vȠ$9/ si~X'{]j _1YˎgjrPKʉS]ފp$ []+e,An(;"iN*rCu@h۔DLgvK+K5cHCٺ| ޚͭ^"i 8e \Oj fW!2dZӰ`jj[:<dkQ)zBOcf9L[kɂ]*M!%١dltvnS"  XDRǰ1Pn3->h6 hM/䈓G 78;/gmيOuF E{N8y;9Ę`,xMLY,J,d_'GD}V#vsJםvgXPmNkq_d-(>~$Y0ͫG.Mmf8t~9E KJQԔxȸm&MN:╔5>N&J[ЍUWn Yɭϐh PR@\ߎRC:);ڕY@p뺴 (xi:nSeIy5] Xs)4g0)@m؀9]"bh'vy+W=(p"O ё,Gэ/A{[( [(jT[f`z Z&JJ-S}Y#'cEX_u%I!8P8l+ 2Yp@y !Y$ @ݢT(Q Kx1ٓ繡fmB\]b-urծ* ;zɬ^Uf١'LPSꃿ Drg1?<ƒ!IAS > ^\tcEM>к1|ɵߓ:' 7u#B1C,6JMt!Lt:&*\-CcݩegN'h̡ qA͛ ZQǪP 1ۅtж{4]0.j1ϩ+nT49}2Kz].=}d 5DH-p(c3rQH]ߪĠR v{H2*'SHvYSb:p{c^ds0%.Ie3j]2< X!MISɌz6iteĘaSC!r{5magzvQR% F9ʋ3s qv>Ls[]}0&L4g"TΓ hsP23ЦeL8S M pAxpihhmUYoߜo8ktBgQ!%i̊)padL >rQD|1ɳiL&n'n#%VK+doһ?g~?uտ2'ZC#u&* __"A$820Wst[:i%VMc6u))z3F tZAf {|/Q#uBFY!X`̫#ܹa"Zɀ/*Mº@!@ y~pO1cgxu׮z呔Vְé63Lt1䢶?5#gДrw+Wd 6Z-kpIL8n+l" F['u+B=Ξ7hRb͓Y {2tɰtu5*>Zr`"4V1kR2\nrEc!1u/+Oz7ɳwD_(fc0*#^: -e{q:oG `VqBKa'CGΎdP?3ދ7w&MdȄ`q6´vn81fWts ?1c3'WrY@@VOZQx:ދFbgf~ ^X P$Hr1WPvuye"&tF] _{1t֬>iË.=uT!]4qYI~ɺ8x}@I6ITl"&;T*&>PaFAL͙@&|;^˙$$>s~2p&MZ/`?F"O@?LE3L, n[MS+S`d/`@UeN 8 ě FߕmT/;7 z(Hu9C|xȉȣ.N4S"uvzyV;qgY?_c6'l=%wQn>!|}p'K @$=I:*$qNw4sz 2@E- V!\v; jzwV CZ!fBW,ӵ4kAD;f D|d\jM< shc.\o/$*=/pwDɒ~ڇQDP[ԑЏtc U@ی5La'!psI4֙EgzFx 砐Pl-t]zuAr "Ѳ3iMʙc:Ix*g:!7m Ci*lU~Dt&P2<0z&B|XN?773}hQ$yύpF/f@Ҋ$0~OajxX3Ŕ,[ 7#DZޏV[dF]鿤PPj v&װ/ 26^ķ]0ڲszN8dAvm$Ka S8,qȯ]ߒcGXFHP9u U}ذ}]$ ^rݛI%}g@IX`!2^,C$f pDZA׌LSs͜B0x 29S+ lr`i{[a h x3QQwX9{A(&DËR &Oo;ބhI &!ŁŚř->Nfj- MG$G^+bgv622wdLh1 y0c1l K=B<@  `ʣ8_ijbE0 Wt"p DiOƢVDQ]{Џ"J tQ YQnMo[^ܛz3z/Ѭ^3_L}>IךmԫYԫKGk׿j_x7LsFײܞb zh,]KƟnVJEhߩh3zvOOAkڲúWUEVJBQ /DAT&"dX(F楛-k73чk}fmϞ=>7ށNFܛTIvֵBQflhYtW><݃+@ݹ\xg.;b C ͳ=& wO>c-*Ba&x`n D0lT]ào6Lp-v\eOB.JP  9xp*hfOc]mM;ۚ:Cљ]m{EA)f)0}B? Yb E “*h'tնW _}.T CgVVt5'z\*0D2XI),!L33L8Y L;E}ʑ4iyȪ4it$Q$$.У$sa;\P '54ft3|}Bl1%2V8G ʸVgk9BǣśCϬyըu)+nG׊ocϬQp[U毝?3 T}W j]"s{C3C}W[VΜ}i~{Kh^Yߢ<bPKkotFʨ\RV.zx*m n%ZBM53ZRl5kƤj yY!Vlu %ϰ_bmK5N%&Jɔ91bU;}fњxQ](P 5ytV! L_c:e8A!v(/@@Uqsh ak`T1@u"FT|ghMp4s0) ż?;F ΰ&G˚ZiJZͭN[>PIfgpj9N>j'z KiVڱs 것h/RT MN$:vPGO1\Na#?=2{bl(TU|HQ=H+,%lV"rb,3†y4r"bpv8e/5lLю0'wo*6`2)bL#~Hk6 ;F6nt^ U۽!$3>l#l1bİgw7@46Aj2=Ӵ;2J $- "՟c8ַ?]&mgL+rN ^m40FWjGm\ִ(RW@ ζ (k[li^ӸfomZWy77(Fӽ7[LS[@ @ %،ч~YP0%49W}tЅ 9b5lFcY9Inl#hf"::((Ϩ _W[-&b/[TۙFP):%T aSpg gZM?ybH`|R ݘCAɐ`3˯$UƁJz*B/T/Qu_= 7{pw /(P94nIGqݢ r)t'%zT瞶g[17ըʝ*Ȫn'W壴LA Ad紡injMU6y09IYu>;~Te@52(rhR"F0t$V-u.j^ [׫1ڹ@o'G>z*ߖܗ~*%{{o3!C >Z(Ҩj|) [~kPp0IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/stackWindow/info-panel-1.png000066400000000000000000002545631342332007200250730ustar00rootroot00000000000000PNG  IHDR,M pHYs X =tIME(~|tEXtCommentCreated with GIMPWbKGDXIDATx\VTɶ9cW H"}dH @-boaS}YVq^u^y}cX͌_.":~~=󳱥;㽛.mM^ܙtrA7վZl+3w/;yQ?)R\ޛ~|>ɋzzx2wl߃ZӇ(nn|F~U)z7{U2׵㺩*TBڭ* t妚lxMcūWrֳZĺ+y=5VwjJ?O9,^U)J%*_U@ӳu]GzCg׆ߺޣeeg,yetW[FM[UC%D"WqzM.)_E.]PwRM]Z 识DU5VUL$P\'~=s=+@N[}zK5ԃ^51UGy4j^Wz= &)=yl +V â~]`H ,D7*Q?#hciRYJ P98A]`dPm'%_hejYʡxX`tWBC]ފYzfk?ASХ+jSm+e j8ӌ Rqz/޶M'sa"] (H?6o0u+db`AO`Å*r`֦Il1vW?L^B'4EqCi6RL]ٝR7 $LܨO6eë Y/11|L2]ohE/* }{)f/ [ӹEI gHks^j<(7 c}PfS#l 㶊7LzU :J"y7Ͳ4z7,8#PL-K]yUʖ'' ,ҡGU^$nڴ LpEPEaFL;gSTi3(8K^B4@=JL %Uī2;2E)eE5#T*Hʼn,*Q1Dvz,!qsBnuK%iqD/G.}Su%su)YDN5Tӛ\m=N/o o31?7?;2?`? YRg" BYZwN>HWw1L\Z"8GU[Ge}1Sg:Zx|ǗRqwvNw>oՂa ˆyXˆR>˛IHy ޶lRge@8~&y䀫IءjsNST (&RjiC"9 PPd6d(%LD5^!EZG@J3=٨,OWtWʇk)կzbk.MC_I/{+˓ݞL`9y9ŇMsɝxkyB?Hi*+Q=:'ۿ=͔R*oU=l/,Ol-+ܹ5 ƥszPAEc§JFESR*[23 tiV1 V}ߊ+)Jιn.-yʕ3qT+P -.U%:"aۺU]Zqz(k>r9]"]0 =ta LDaTUwBmvxo |')ϛ kn,T7W{jiY"R﨧l䫚{=gD;aOh{rw .~Xz=4"f|1Df<`(YMަ.D;n@50Ћ\<6q!#S|X3G&N %{eo ?g2p VCgXv0My^)׆Ø1Ew0wɘ Dۺ J TPг]=ix8:dXs`npAaFpUu c 8MƋ:ŨVΧ%o%}_a֡$+K4ge'/fB+E2µ":)נr]v c#])uț90Ѕ3HbY12U/ixw_dnCX@GVFfOsVUŔsY,k%DHF`64ȠaBX6ʝኬ]36Ftߣu.^\mims 1; 01Q hߕ'cvH:jDƯ.2^j&kv&O3 cĺM(8wEJTĞP=&ipıVbWCc`(iՒZDHZ:z"(L]Xz/ dsMy407»Pc^."+`:9+%$=S"_h.(ь'!"?a͚BI%3Dw}βtg!})~V%$eY b00YY_EWAd(ӵh)D j6Ux @T"Xц^tL^@<;݀{pf=S=7= aWf RUDЧn[Y|X'p0<(ilxx0k(3|&Bb3d؁mĪ-ZMZ j рcM*Bh^82h6'-^7>:NtoƩUO71seu&;óR^:&,cgTU%4O+I)$J{^gVZtGNˌ +.i lQt1i)90͆! 7Y|^+RX@R'Z~ $JR *FOe>,@8 x&5޳=v; z%)XJYf^ߙ$"68E1gmX*R%ix_}Q|ԳFc|Z6r{^}!y"" w%RPfM%+aq1 KP$7Xxsdp ?;ޚЁ`cyZܴɹY6q-A 0!1hKf< IOҟܠ z+֫Щ]NtF9lĄt eXʈ]*6JxG>V|5 jbEQ 9fhDx@{ Qo j)t"Y]I%* h8j48CE㳌o桋VTa堺c&:U"\D8Г n_KOG[LEC\Z!%l&yeMӖB ,[* sTQА_f(pXgǢP@!(0 Qճ BMDl͔N$Pv*B6/ԯmV#TI 9$Y{rC ɵYkds9fq2`Z}4, D|f9 ^6ϽF#̶'E\ ;V0аskr7}>٬ica-/@J#W2#%^b)Q5ݿtsxR X4evCy7bx EFh; LsEU45@\f:hgF4Y*jxSaep 2Y,:(3jۜEΐ̜&+pa!([7Al-q:PlW"A`4PZ1N'.ya VBqHCd,GB%8$<~^ ME' cnVNjnP0XLnd^t<QEN9qL8/ۈ# K=Ǯ=Y,!dx|gvd/"[YӒ(>ɹdI✿e6G KQ8b,AKG˛ȄWB@aPsP!vo vH >Ѓp/6Gv$ݞ=Z>#+V tP7&ޘG@\2`!gerqe7yuH $ T %i4@gHL4g"K<}YAz $} Ĕ4  NL&gđFu(!'G^Cj`TVHg!Xbcߠf`;0ޅ9 &SXES)[{EfgmaGH`O:jbBb0/b%(:Vdxl, l_QK,Yq_+!Ab)JZ9z=DSݒKH3#b&s&Hr~AP*n!Ke2+}ëݭ[z5^bh'!!ΣrᛊpѼ|͔GB؜[6cQzacJHF:~Z `: +dX$*gxsC/=&¦ pHTر=/bJ%<=^̀є ê~¶aʞggݻ]6dyq" +2#'Ӹ{NjQlJH9;6kz={`V@, cZ7>hePUYy ~RbCk^52,Ê]U˜`_qj0jRmlw D*֬\K- VZDl3_]-:̻SpA]jp$qiP˘uD>:m`Zٲ&KmD G, 3NDYbf"zm7-0p+ɃIRhJ*7O]74pK%|dž1*$RV,a5eg{dt}=M gO$Wc\ &zvg[Oq[X7أi2Fx*hbcȱe,>dz+ia`5k@2ٚA^HPMUqΕ=}$-KϮ=Ic{aUl$ѼG@Z= yLBD-xDkeq $ZoZq|5kLoz}oĩ*IBcEfk+?H(vrU% X0ƺ{ٷ~-XӎeRPVLc@Bh(̐e!QlSori䕈t;iy zcݰN`l AN Zm/e 1Pα7[@x輴l/d'YЌӂ!>P"=3IdS (>2A#(p.,l h>kۼV:Qc.;k~l>Ah}^D0ua1*9D&ׅu4"U(ָ8~7DڅTY.[h[X0k,G$3D`AX(##ѣ9~{ &"=m/& \fK6$q=KZ; axo߆-t\*e u,DC T:A܏+WX%.s<[nI  #K& :JH f2P&ؚS P]j .>^+#|ܬXGf۞h;O" K ݡ~LKAsRcYu˫! PmհkQʿU`{2TB(hh⏼3ۭZolP Q`u D B暔\li4oȢ5(vCVކBJe9W^eEǃ) ~lJEKt!fiw3!$@‹͘ZOvFLbMnm!Xe O5q'tpwbu1Is0&5 # @YӍ߳ZbߵP4\ql67p QbJ={Ց oV$qG->c,9Z8dÍ8CmQ-|y]#Uo\<,MeV•W A> ;2~.6 ˽%qtVv(n/A}fo ĵ#!iA3(APtJLhKi2(y|$(jx WI$!hcv/Xzecc7WKr@Pʞ8y/@!,{9jl zʚ@{AWO~UBㅼ$:/wv$ռB> YR9De:L=VGVۃGdwn~9|8l;;HE/Y0bG$2 eVm61C3(I,"{擋Fecœ b宯G"vf/BYÞX/3v8~9*č98Ҏ+!3T "xp(U+QN6 %q+"0oS,~=$`}ҁzڋA%JcP wD @$H}P D$1ȌX*Ȫ_|m|w^(l@%p KBhڒ>7sC4#(+ O]6O˧Jd-U|Jt5$_|hr|w~xROzw?99yC$ćˇN$ұ>E3 >-ZB,tFK|H}9@~nWE==t<%;R'R/>S6F L8'",lz}O©?5|C^?oNq(q wQt^o-nM^pzš~1]UDtiEqx ve$Pي3Yg)˒bX>8>ov'ωw`cוK;|yjbSTdߔ4p)xk湏af`|=Rr}>Up=RpPc|@j4)֐Jޠ_{g|5!m\qGSڞQV*8Bw' (ÎV4_ޝQz ҽWS>['*nkiv5s'ȵ]C8sέ;ckc/]c<(%+%"p"Gn'>WNQY c.4K *}g9ċ ^|o΄L U@QԄ>^B)G(@QLl3>WC?)r$6DzzMk~QJdtɴE)Uy5DWesb&Jzd6tݞ'oݑHAAxŵ G) *6{Xp敾CRC["<6oyV9 d;Zq,FB UVu :P]ҀwTg1܇v,ȃJl>ˎsl4ׅɖd:ẑsVCeXX rH,>%i}O\Tj+;۶Yogp/ΏŽN;X> q*4q8|$,~-N/oR*IADv}s0,'zG5Mud+J:/Lb[D|ƹw|jvxYJ}~Mfm^gfg/ r@k=Iq*U !>س ҫ7dKb8wI^-v\C+-8=*e/u2x{rQkfTs =%ddo-CG*'uU,q! )f;\ȣ ~fy~%,[0''r^kJJ>:$nIҾ ,6eCg4<0r,Bs'!R* ]Tsb%5;f{=@f̠ЇAGT"6n.A=LC$J6Z F#$*'oy||c, Azl@cg0 |:#H)W9'g]/d8Y LeLNׯe/> ZՀS_'n}\D_>G 7٥/ȫƊ*VPm*Q^TSCq!'ӊزv> 8+VM=Sq 6K|N|A)V~u(/ |$${ hM {a"R9/kJ99/gF/<lQ1b%o5;O+K M|x·aK91Gabp(Z'̩tD=2xJ]۫c|,9^ʙ:3 4aU6wfO9V>Ie,{zAPű0XG'nr&`pfE)\6H➾9=H"Dy55*+a;6,KKZݙ)I2$ I $(z#шވ(RJJ*˭q=5]ezzfy0+{ra nD\'goMo<^!-?JA:,~g .`َ& Ӊa’bQ\+~c`=A/^eL xxV1k ]- ELYw)O V%MJx' /fyc>sx, ,SQ/y^r(p,puÐ/RGI}+x?G`b12B#=jaN @of ?~ $oLj)AMHF [~8 ncg&jf*N\#܋uQE$/5z 6cf3 d&vqQ{lSh/%^d%V7E/ھT|LbsmX#'#O ˲~ U񃮉 lޒ(cM0$ )uG #HE%g5HC&ET]pz'g^ \ C>^'m<|1:$92]:q5t2 !9X%h5H?Gkid@#%iT9hsҖVЅPdJ@9Щ_ 3E- K%jr`zD{eq>ub\r鶂 a6mB\)v4xBGvdСZұ)Gc$|#Z;+E"=YS ؀G|fSs5jXNDfNntJN8%rq.]Zdl!ZJs ܀aϸu !߈7s n0;+Ƨ$zH13\.r$v5yҚ z|4)O"ʪcV.p ZXr&HU*D.R`I7'_7{>w|tۇM霶g3/f4M^qS{}͹ hߜjlg9^?h;?;ZOQoLLKM^ˣit ~a~pJiziW_nOkhE+oRczi$jJkۜk m5?e/5S` Lxt0[5Fj:5X^Dw^P:Y,qL9kbIoiaKAk> D e~[+qIy$AiJvqK:NCiT#3SZԇVm%%'⼫~/Ael߽bΌҵο\gMLYWIfK' bv+E뺪>L;-hvv$-O]O=랚ܯv|EOgYjv~-?x\Ts+uMzO^orJ˅ԫRjLJɿ][~P/v-]{cW?hOo?h_~t?vԊ^:kL4xKc~M?;Ҋ^阚7??l2;6,=yZ1dVֵ|YKt4c>ٟ?9YxDD#ۢFj6?=6Gj{bWXDGӹgQ[-,ЎaZwf昡'>޾y]j:Mɗ`̮..R]lϖ-e렷Ntn*ou YG=M)떩t~9(|mK^^Xm~|/Omė_7=l;h~KVӊWr/|~<2,ZjfڗK(QضRAV%>[.t[3^wV W" Fl[+݇멳n8Z' l4&qCkIsػ M`4YP+`$, sV;S*3)E+b/5t˩A&4CBlRz9̅C |N$q&dd2.=I<،MN JIR:~dv-x%p #RDSeRN. a]!%*E]Lamű8I$5qߎB,/z9E._ |TQ~cCNrp]ˋhácJؒW.uIw{;ڭWo~4b܃}}~Y`b~A͖۵J??ڿ?1V~-*k~vkT83-ĀhߍPj/|kl`_Nͳƭ|!Vij[Ntrc]ve9vdCWGKB|WF:6Tn6GMT3Az$N?.j<(|M{5BDod;q-bb1G%Xfq2 =4 oBT&][nHIw@INM+)a0;P& Rb .w̨C/c5^lE`H;A׻cⷅD퀕HEB*&q$i#"1 ISND6$ b1>Xgh,ÓhW`y4̄h?$71DF2.Vu]Hh!Af,xT`!]tdLE _1) oU:Q(M˘8~e˯-kߞ~/iݾCu1|2@wx@]1}gzָSd,wZy޲#{veOjk+^V~ES˭}+ZZ:1TDmW :mDnM|ўr@rҊzO\\¤A/AzdfX%M0 UI8r4mM -Ț|H7K2|dMQ˰#y'Lb .o5 9jJTg&=ݪvښeR;N~w 7^w>+kcsG7.Ywz'j)Nç DYQb(fzʨܞ*N`Gr=Yd[>2ZĤdƖ]&tiX&lWErm([vʰ9 ]R xN i\m^V6vdL#`qDyVoʉMC3+j)+yEߞ[ [4X23ڋuS_6jˣ/u_z?<΢]s G9u4'՘?|?ݝdטo651NauSkn:5 3|K #-/(dK@X "W<.ޣڳ^ h w@ : 04Q#Љoż4 xRؒQ>4(&i%V([yPe^]3#nɴa*f7kx9UQUT|0c4XVΣb畅+]f.+n5KKŎ|{ffJ3X] wxao\%YǓtݼη{}4KG֒,A˔.8rx?;1UiR ߄Z5X^WR6홎C'y(dآa-ւhݣٔJ1T~Ϊ4ڎhoVA+t EĠUk j#LŒuŤ(?}vQq+D9֙(60$@1ip;n{ݛ"\e-bTV"[Z"b-8(~PyU?%h &,l#;z>gEQ9J8$+Jq's im+o$dn0o8=Zr?1gLrFOL`ᜇ o?Bu3'S*6sm::{Dܫn=LLvs۵]ֵ띾ANssl`B^nVt,Yl =Ж.8즳+5F"b7}I:C["KӃ =Q 1᎚lz>'趪(lۭ'ywM\!&`G(fiF^FNɐCL%hGH(MLJL7bXOkw+sMؑH\E ݌-7>dR;ڨϘ2!C's5:$tRn4vw8>6Fl` rn7uIo8$/c2x;AԐk$rc# R( _- Xz-/rq:lFLJ5y =*4̃j电ghM>ߙօ!h2/ej9}Etȴ%K6*۸JTq\:/1EXCD'5HEX_}g\CSc"c`a#OVl4@Hh0 /y% f.% f,gS\%wGNzLGE77QzUϺ.-D6bٚ;b!6؅Udw"$0)ف_YC:_jL%& iOhn^l.@N^,b\I(]&lP>J?SAj5L "ݕ([JHj{>J_s*di5db[uۑjr8b@KiCpU8o'Q] ۜο=[~ZjG:'G[uMzJ M=Ү{׬q}?c)wj;2a/k~9GhCJPo$HKyuZ88!RzvSD캑w)3}P*zFzټ!"ur1Xj\n+d&Y[]nTg&' ;6e)݀wGkI6LT,p.ʁI/8@,,ӥjT=ԔB黙ˇpUVrnQnbT"O= D /iCC @"Qb/8:ZGA g]cuv7' aKL15l=fK bIlXnix&q.|+@TC4X1'#y˝ 뗗QzuI+'(t a:V >LE!}ܥ([vE9E~3Ǔ tgXN;M .Lus7?htO NtYLDϞ^˥%Wkl5Pms%d>mT^l=hn!CSȞ/'# :`73[˘@\3ѯ_އk]_%4cΌy@u{ɝmb_;O,nhne8KL3pQ{m8V< K#=?].pM#'| $ئD6 v!VmLĢekDh+nȠbqeK!5ڇF 'IBbazDٲu]A  LHTA\'9(-SJ^3Je8TEFPV8z.F6\bc6~[ψ@>a>iǛ >}#W%gA>.Hb(!9G z@VrI,·uЎ3rz̽咵'oωZ~͊7ǻmrw!e n51SN]8]+_7e@hՉ~58i)T nx{tMs-W+ m47GZf1 E+QXrT: 3t+N&ry*E&EKt53!93Axg oNȣaRK)9">y%;ú[(@?]Ɖc2"N-DYw׀5 %RjA0O; Y<>1T@&0: 5 xaALF8-RV:W"2i ~b¢ h!Gקf$Iwr1Hhs2/-"ESVj6čmtu5Otrtߞbs NTkg1p֛2v@I RX["Q'(xX=k^3G> c]QdPII<Q2AzQ%t^*t]5*͏ǔ~d ԝS?D\8$tPBXF0O$s]jH$F#f$lf$t{"1ZF}w2|{bsw2٭A^6Fe,n>i8vZu orRᝮڪtzmCmctO:,Z0Dl@"ILt-G@A{ %KKx4&r$tή~-dpaRuFPtkoHj<'M8,&aA;XZv]`կ^ ]s{P8#)"sHPlq52JnejQV=3Iq;@a" \@iR!xŠ m~?T(EwGe%fﮑgì$:TF&U_7,A)]=tWH2$6,JpD"K*ڧKOztc#Oi.m CF#x8np)pzHw\\&ϙbNQ>.;nx8ct7'97OQ|"DZhJȴi Xn  rHix?SWԼB /Z̒~F{|Vc7eYU3#W(#RTaCqtG豩$ۯvOWG98;rvF סbc`}i9_h۪fʹDtEi%*,ĆȺP7;u\6?ȟj"yBoyrD +R8Ljk =KwTQNk ߇0u_$cbRZk̷Q)GsJ˅A2ļPOυC4Hc1 G?8hFIWN}~ :XP6Hv$)a Y,ׅk@Gǧi$b^8gpHm7ɼ ː[BJ umD 69haD٩TVJٍrXצ[bǚ9PJp\=ww{k2wBo;9ƽlW,/rQ@fռwPq%pyqҳubGcw9~-=Gq',Уhydu8hyՎbHhkݍ]nlmG3w)7]n_Ă-f." A rQ44gMKK/f&Ha\ԯ 9]j֝X\)eL@o8) -l}V3 ,&1_um:[6z,+u6eg kQ`ѥ4 8l0N@]@G̀z Dq>o_ v:p5/ϹI2Il {зg%}Xp'T"'mfAܚct-M澟-57q{F*Ҿ~u\.- r>ATYq9܉cXkUkvtGQѷTY?[5g2^jZ){݆zflPF~Gtr $8@ 툀PPkP='yl"ԥ _@%׮ca|gF?oAʏs PjO2k󽹗^S~ISAvgco8ۛE\<]Bi+|x\d>VKxHho@A m7de!27|zD/]H1U{VEv.ڧjd@ޝ1 ]CT*F]ad2@m(ktXڣ2!̖_%N$RĐ}H`)9>ÑD\4"%(i0W1hOf>dː2PTo3[utLAٝ tk@4E!#T+mUr<<&. "o{M! c%pH,`cd![b^F*},?NJ$q߰7+En997ysM_vG3P,p03 0gz9}e߮5/t FnhL>iNUcksZN,3ȓRaDyTf\ke8+#ӜYhC1@ Rǣìܚ!s!IT2c"J~^g q'P\CL1;> D|,οcXy;VM"烤{*Х(WT!E[( &V|FG4 joFGХOMgKs*Ѓ*- p4 jM'`we&]KywI̲-glS)O ڎ*!M#LX/Al >hU@;sSʝfJޚ 6} ̒Q;UTj~7yINGbFJx1Y޺[X{hp!)GҞb2}5ˎ׭K9ԦC?*NGCd1Je^+gl`9\=yO/ʔ6$_%>3sMIi =oW_dB 8 rКC#gvz !60QhBI8C6'K` ^s!587 g]^;D|oo}.#Brܒ&&trt\MtF&v 9ν Jb]a8^"hA-rDM‍)B&`P4RoD"bE@A@;lFDyn'S?v/iF-$%jFL4piNu6)QSXSKlun6͏^RrRp;䲓9qjAbVJY "cU3/,LBg]IK/6 \ɞm9rD.aV.4ޛ[KsɞNgNd{flդ>\% O z _1^`9fELY呠R~!6mnْ}ӑ`=D~.D?=(Wͬ\r#_) U3.z,!"ʃ!s̅ )08 Zgh\F'_nrмWMZygghpCfd@#]"HyBHf~TKUw-7A,YF%#'3AٳeNZ:6Q?_0iè,d ײ(2곅ۏAO _tUG|Lq*g1rjc>1Z>~1rܖJv)ie⪓mU5"S'mm*7)hM HWaPx7+dBQmƷև!4Ǝ >o9]udVI'ڔFyKĢ:ko.E[&bjuNnq|6s*I-2Ri9fÀ".U{9m͞˥ |_hŻmMrQ~{dP0 8LZkYԴ -J( ʄOD.ɥw㫙d4Bv![cucƬzLB^T"gT4te#hG`85'L|\ѽ͒,w\m$\,ڍ |G-x:ȟ,ȗ5 Fn_`zceV¢5D>^[ Tzscldw5 ngNLa5%#Îu CɲW Fۖb0s5^oUċ^6#qiBRp]n9Y沬X/}BEzP_saܷLgS~x.?nc0K<4{v SG 95յ9$cMboeoSН8Z[.能_hx'Ī%Zn 芏u\ҐMn@cэku6}jtʬK:P; w29q"Gu 8;- s 3VI)"7FD'75l0s=x;qHۜN>\xt cwAaѝ:Me%(=g10rdVɬ-< G}٘t[6Q!Es)7 䰤dl3Iwj3{t#Qa/)5b@H<3d/0{^%kc#hVjgJ:K>ar⠒Öuga0SESHlY[MU|fr #-)Ѣ6jE GG=1^߶i1(hH$A)IDDR Njvv>,t.TRZL=IWT\`NiؘY2D V9C2uQW/ل84vGSu.$Q#kʫ?Zyjck!)qF5d6+ډaIk:V[_J M -ac9GȀ,J(?Q"ߋ}Dദy4R:B䝭^/*YYowh6'wu> mAg! 3-D.6MdicD#,-13Oȉv]8u}bqIu"D"62" 85C! T ReMu'V2f̚@$MjyI` 7d W )+`Ϣ8Huh!VZ2[YPMyjdbu}CG%ryٙ[=unH /[(v5]uR}3rvr_W_ufw3 0xJG$0S6 J6DBt9H0ݰ\Iy0sՋ|D8c%Y.A8>Ά \>o쨓6-ɟZ!xgGތgw`#֢/X5CB|@k/j{=T2F ZRYpOH1 WTkSypdmf"A\脐{Fm@6KɚQg dXQ.옮To(P)\vlڏiwE6Mzyqnbh (Ìv&dBN㺉Sayk략=4&rÂ+G/y"yӦ\^u  E7{S#fLBxT(#',{n)Y8:F6=ML0ϛڱkWJ}lkYGkci KK\B*jub(V/NՅsJ|:ɈDWƼ0Yŕ"q/h;0G4^I4FGxȏ^WV3kAFz 0J32ƃc$'(,6YU '7B}=Oޠ8_sG54~[ 0l?釨MBED(Pq1:l; 6A?Ħ)\i傞ה} ` j"QAcf,~pCo\%]A(P^M.ȸAW{P$SL͏5'^on=_ OjjuRC91wqUUD fe|w[WG\%r殊hXqaosqPXZkk˪,Di-˸C?Y2PL,BWvf5crl\6Hzjd Nfit=Cη-Xođh䰣wӍIYbʓx9R:;58Yw63܂-"LQ`% ط)Vȏ86Q$ʖ0J+ cnb⦣55󖀂FOVFb 8^K.kFo^^}X5y4b fߍAâSXb׭] tQ6o:[WZR+ )"5ݖq=kNuU/GPc)O}h8ک2Z˼\>p={\@m0")` "@vOԲ+XOr>e-#z|!:`L-kZ[b`6& }ll60uC1W9{DNN4 8T[I!flb.4(yݻh"V -/wZ𴌢yabHQBw!EQ-U$`h7E27Ĩ42TcTyfj'wܢ@27&QZ3OK܉&*W/SsHQʔw%՝|vIy$:"irPp 8aKlQ;ڔWXi `sHK6tMͦ !`DuY @#+ 6'q75hZxs)6!@e f4âihWJ9ܮ>c8e B,/҅{s8WCoIj`GCSa3qK90IuHêH׽@hQbp})ˋhz]2selw"!1 ?O5 :_|z6Q=/$OYg-n^Žrsr:jɫNâĿk ؉(EhЀ">6Е+FWSqcvTx͚[檝?NI ~ux,%qHV|ڙ+y\qG3I99'߾I+`ZϖXiCBMn@gE~/w1-8&E'?@͠1xbu`4"`\3c}^2M nrUKaUcf e"!UdرQ0kC9@ bĹӎgP9Z2V|#3$H7m=78'K0 NMwnVtcUcs$@IT .=id-P,X&Өɇr&Zt+ 9 EkN?Q45|7 m1>D[!C(Ig5B6"ʛH } n,8 f 45^7*^8 ʬ!4^Bv pfCa)`ZAkEV-$K^!vǷ4c9eK` Le1ޱؖ#UEek)K@ryrFNL%&X$rHzh{Q2V0+V{a(Ww&ӏQz008\U35o0 -5E}?Ĥl]M33xi`] 3{y?`å-'7V <&Ww[!  80[! aVƖM݇@pe'Bx-Kȑc)!vYMU_)Ndȼ؊5fX'gc!;r&_B^(o_|(Lp0ωw4+"jl809 9x6dowz^-uJNJN_NI )nQPRk_{y=}fԔ򲗿j$9/ІI$"߯ J"in_>BӶ &J̨ 2h\Аt"##4=KYq жo$CH^ >66Cyf;NN>ֲqʦ" cIrpD"< A"?"9rwlRCN$ڟ^,^? ɏf /o>% ثHKɒtu%V` CY;oUjb-yLi;K[;xYYjf- ;d5yg;^Hyg7W~2C ^ ~z+ѤO'pb@Wx8 3K͐o~O><>+>  r$?q­7| GB!٣=2&W6bT䊑=%LHdCuE>WK̑;,ݴW&e\FBRA4Cνdȯ)828tiI-d|{+Wcv "qcm"ϚrGJZб;kVeKKzo4|̞T*ϗ:]#~.$*6|#N""=#Sr5fJߥПCWu1Sӳ6yʾoWrȳ^sAܰ4YHk'꩝>e^u6gܞQC󒔿&`Zσf7Ht.T? McH^=D~ TS0%`|kx0AJv*PIr`Y GglުJ `o!U3l mZ?ʞdxK֏W)A0ZBrcϡeM9ц#`7s]zV/E{j.|Cl[Á4 !@&6@'勈L|Z.Y#6)MEjb'=z6,WUqKʯiϭk,)20UĄ Szy[֢*)by;o6lL^󺛿dji_: 5$ǖ8аbDH`YNbd t=t'ia3kp)˕m7rwUM}X|k1yA頓{`0d+HST_f BnkV""߬"oT Iָ]bДRv=D~.D=.vC7ۙh'\zh.M家3e%Ihr ̻;)6k>wgٖ#X6: ԣ#x1L_LMoW,GբAӫ=P>60 %Z<`q)( B jc@ Ixs񰨝 jvgQJ NNЙ`qNl؎Ui,_gݝ; G$H{imG XD~RE7;ǎ8M6=4w9gBuO漍M]ɿ_S 7Čd;أbٵjS\}tk{\ph#2R>nU +hDJ!zCZ_ϐt:8w y߲)s]XMWԹa9)fM?\H! >6v5 ZUGfL7- ,u!i;CHrGj1y Iy,,BX@W9*_>cL~r,H6k>#χf[V7 +3OV3^մnaY3_/xb9HFG}[$$n{YdAj,u6$is ` aI1ϋN듷ݼzG ڨS'o W$֊~9uFwkmLUuy!U p/L[Wnno>Fe~sh|SI(A"?[b)31v7z *K1X 7,/_ kǽ`_3L5MǦfh0j3R.I]d`l /qwn/q- ŁӦ+Ld~ga=x4J=]#T>`3Jr Sꡞ[$>*cUw^dKiw 7 o{%KÕeH!aGĊ̙ `{FCK5]8op^):?vf:ܐ,RP7d?zu/W_]/ /U/WWI^ByH^s]A导_l+o!=>kt/W/zYnaIםܕa9'A1%@y D@k4g`cLh6wdA몕}55_2!$?܍.X<s!xWf7CRȞu<)Add(A*rkSh- YY]k.L%7YGQDnc0c1xl{ ImDŽTK킊vf@M9eK .N((he%XɦV.Y|\a?;hYi~Gߞ~yڔXI]pP5 ?Կ=}sPtڐ'W6Ǻ[[Y&+y[&ߞgU;sΣcC֝do$6\1*sh86=1򭺍mcɨZ-Cr{2#FG?,0KM ;O@6=&wfu4=G:E &-p@Dze+P%?E{6ZGMt[~0cs`ru8FƳ14P'uMfO ec**rr+ P95LӜQ[gԾJ#xnJR;}~Rnh.Bf1%KX{dc+="l$:+^fFТh6Mװ" s`G4O'GB?EzpN VFжRrӇʷSʬ'cAt:@ۚet9^lzEL\DK;,w[ٕmQx}T&ދB\|F nxQ ʗǪ[[5gq^djR a I^yZwDY ;r9ܶt v6!0 81E98ߏ/v>]guE`v\Rh Ar0+Вcĸ|Y cY&zwzMU1N͙":1IFl6)qkk#8J3=a GBI,\BG n.F9v;,f{V-Wh/nӽ(J8Dz@,Hp47nG(jcս8E5a@^LƜ 8@ir0cZq:0m1Ҫ+V!Ozf,_++ hʻ]rt] [AWo]X[DY)g64Tu+%G80Ly[5D +eK/ "eKR~4h){%Œ:nݭ޼υH$*es%Ā`1 4CżaDma$1%f`)D1${~2xa#(D!dWRE1zB#8[p0BveHǦ1ZvL}=x(ߊkb'6aI@{8۶m 4H ϭ"c#\tf_ykdQ#&l\8=ݙvíkzyআpE˒hٌP X'B -#?ټmnx"7Owfvk8nMc>uҚ>nN4NO3Ϻ3>%6dJ~DVu[GM]Ansgywf>y֞dɳJ֗򦲉/oq:K"Z'+ƹ rKNJ}i6^7sPgw%PE|yS*=D~.D.A]bH#{(%\EOѪfWu88kEC ]d1Ƈr KޗOP4 XHk}, |vZDkzkTh^;Ų+8@6@#b1Kiaw"a:Gf_z2Jِʅ ]+~xC NBFԕ HS(m$d sŒ&[S1*u՝ 8SdIFd;AzjISɗsxQδ)t4:@Bb)*f4 1 -N{ε}7w^7x{W&}&""+++ia'5f!\[&c(I(PXʹྩWkqV#+ mdf 8HQo;<{UTݺ4yp{"BuL0;[6eWK0߉V0҂{SMǕK<ia|9BýWΥoK/&C"1=zT'6騦$\oD3DQgfc'h fI*\"@x'''284| Pe.4"ALe+pߵFə}V$Π}}dJW51ln^ H^.W]Niʪ.9 FO"5BLWPRD-j\w,PFMA32d ނ{bW}3,$qА.H}bbC3Îek}(.9ЬD~2D[,`t_`sg]'Lf "?g.b$.h[כ,*=/ODZnf }{Lka8nM4#鎢(uѓݍ|4d?FCZL{d zAcq3n-H+24 %}Z}oAԓ Iz'9ۛgVCCrxux3ڸ'/  FWz H%}K$$q=x(Fp.laOqfd0 m' I% y ,%E6-5fA/z6ܛ%}##YP禗.eLت/ V=hb^C%ؗ+q8mBfVJ A]כ /UGҩ˞s>X+ n{ {Of`t!\o/h'G@9jmvଶZ99YyЫeydr9iت`x1-q}Х)ٚ{9$ CWmIY?Ӯt$BAt>X`<3Q 3DӀ./^285Zn@4~%y($2GUϯL5ߍI$H%j@@s¢C(1EI<|G#mB!})͉)8r) q2C鼐fu(LG9}ǣ1EvNfW.PѡA>k;a?bl.vA!ɀaXyUl<E /L=n?ub/Xc;-Sݜ@S*cA% %t]qW1#L^Z-01WrӂH,,7D24s\LPgZ,3C8M4 1]{V+[{-[1 esq UmZd8OHB!HrVגwk<$D&gRĆ+Z힧kh:b$ 6i%l.в[<4Ȫ?z2=Zc00Qze>}`w.*R=f4zJ:"ؔEOg"rqb1[#`1OqAX^ !6l!(M=G-@VV?+ 5o% Xfen8,5Iޖߊ]ܦlPWki l%wF;}0}_%ol틑~Sn GKC9?UGnwZ+=Zsf ,X {QG]1DK _;,j "?,||Jᣤ#Cɚ˔XFl0n^JgmGt`@L;K&fUvLGa ˦@l܀,][8X AVL@,W8 U{&]p''t~v|خu>M`юT4g?՚1y`~9ǮmYOv9o hG: [cښ ۴Uʚlf4!p{.=rhݧri:\y=\" XB%EMaw&'^cff }nF7;b^AA[^21C—mrme,z)ex:kn{lwW498ty4N5W 7/wp a_[.b7NK6h6h>^>+XV /) xh3,_O|SIO6;5i_oݷ'N~t 04=h f;6f?^obLZf`O}r")D?iڸ mM 5}S;}@2X!E47& Ç%@Wwq)C5a ! M?B͟,~ZN#Zh~ل i4mn[L ]>mG+kVGK;hF?Q5':M$"2ס+Qg.E{>ix&MRJ6أmclcŊgR0K!t05`7<t|f䚄V;WoV& hLfCgd5!m=߯|S)ŐDjnjTDe /;IZp 3 4e3.|a*&\T&@נX,]"p •ɰ5zNÅ!Ηb ݍOQA1@=]Sq+/q%t奺7!.(a =$MQ9EM8F$ lDP:X(OFp=9xv÷ʹ{wr@e%-@۫-rcWO&Y6F:,)V2=I٤%d<$ǷIna_%Aʌ GKwVB2ː9}1̱㣞&a3&s |㊖ۉH٨IR5st:w}ՈHйKmԉNvv[XD~*Dy\2tQu2 %Dx2=# J[219W> Rp``V"&m)#0t=opY KDg*/cLڠ DBwHTt8kN#FiaXS"d^iY^J,Nctgv̴oFWbܢ6zmPa=]sHK5QsݯkZG@Ce 4z8]:K7"M͹lN.Jл%,Z1N؟5[Cl4͒ja&1gl$F5ml(L][Q޶&EF]l85(-pQ6 hruoC~һ$Ft{r7)af"w І ϗEUYIޯ-tZ 'd2g'r΂=fDGHtrf n!̎!f5?|ޠwI N<ӂ[M'ޒه1Q6\7nKj#Vj3u;yvqːlhfE&ND~FwwKݼVOnް%2*Q}8`?UV=ڴR[H;R!f1uCiOE%V7B!9ɓPC'!q.eWxSz2xS? :^1}ը_2xVV_ c\FPs}X .p碬j҉s2:fJ|o&>kȅ}z`"SQ7FO9ڟh{._/w?n9ʭXXU\V͹;"uk*I5mj3U8hUwHY,)=`ǎ OX,Gf"^R )-Fn_3mc4j1-GC/0]cqG?+ ;.J^if%D2<ˆ?g<6Y|hyM@zp=L**<7Iu:dSyTޚhUz`ܻDIwĂABVb9f 9]$;Z^肘nZQ4ݎ(SD< hh,uܺ0-9wJPHP+=- cEnꄐ41GFC=:ueZ픔0ӷ#s?6 -MQp AxH(t6^>qӜ$4bۉX5ygi)pGD58JzDI7CEN $fb[C/EbDLOq*A[vI+[Hu>)#J1s, >${{&JHUQ+d@"ԗ>8OxC\2B e"}I!D0V$&j6(ϮyUD3/ }n w i e~NDBߠjc@Փ.T :ἁ^@p;s"ݤkVFo@4pz$c䲝XδDFўtz(uH9Cw#b_{ny[:pa揝iהw>®nQ[cɧ:ySh՞ok7C5A.Ěw~[I B g5:F2ځ"^Zb&G ])z\'4Dukj'wk8uM:K4/xb_\ι7\54[i融$LId6 [ܙ4QAm y/MVH]|L /]GsttP53~x(=׆ƺVW ʰ=k.:V$jx59s؛Qyʯ6FɚQvvqؿ6lƊ}ӘQЙeE 0! zZ&#U- =N\#Cp֗qW+uIFyv;{T8돣m 3T,X "14n94֘L vb՟UCM:m祿H7sIR+`@:a5LOXN8ybUwVɽYK\ ya\HF' W)>_/t QFUP2]*M39 |~HmG{{8WzVmf;7^0\DYZ2x6<AۦЙ u#ڱ8phxyH2Bb $AI)xz4۳1ѥəoޝ4񝙛Z;o$4лtp7+}µe<_߮Lw_[7zҭK+mefX/+:=S+~{>-é%A R{ْoyK,P!xBTg0`AB$NQF/ѪϢ0UVL9B:I'x\`Ë/B2dZt\(̐z]X$9q;кxB#724]I^ &'B<.Cm\"+L|8^HҠ8CQoR@$^\;-ߪ S!rɊ!1֕%&kGid1"OPs7 [xZxXPnʀ٩p Ar5M]wTo8 2җr6vdКB%mʹpEHF$ rJ _Jis[2gn'SQ$$PCH$-Y~8Q'JVÕRZ!$}obuN{; \C]Np hJ ZC?o]Z[e(!J3ɡų&Iv`2uu箱"PN#I+Y̅w,I6ْj!Fc:o$1<ڈ[]I+|q^sҞscyhWE8*AWϦ.ѓ 5e8Ov#0L)oR}F㔐FJf,()ũֆ=^I㰡,j̼wN06= u6E$~WJfr:?|\d02ؚײY 2h {9nʙ8D p~I[IH^Ub`dǮI +~jƟHĭل;WJ$ptOjCPqHӧ?%w %=9o p f/G{ H|Y.:fn8D~*D>zsdC"y!\hNPBDlEn6K~ݜDVv\k@D|5TSfkKIEdpi-Zϥ#u Մt4g%5'Jɴ_L JN։1-1#aNF6q:d,z%ə!3R4LjwxHd[tS=<'YT9,$>X;2XhopctNF5:FtH'/EWhaBBz>XDŕ>29p¼MsN]Zs+J;qOYv{=㣐ƦG3&X*&aWPcձPabMo;bb8B@V&V7X) v|qgX cL$s3QBa6l= k49><a`PplnQWƨ)Ѻ,y5bQ"9,*JYhCL Z ,N5%/Mr8ˍao[nE q"'x4e#!J̤Cf8 ݅=?^G<hm뭱}0? Z5)SEЯ[HDkmK5ly)` WƩjMZzj PSbglM[uD֠- /qL@j}MVgǫOIVrԻ_*[!-GV.$vZIa@j#Zsˡ}b2P$ol~1%&a2t匐zŸCDVl a꒣Oqk ] '>>E C 0')(P(Ԅ'뻜3vGרK6KЭh(7!?jmHi C !L`Z S!, ̣qFk?9??"n Y]x‘3_#u[ AiJ,$10Sw-m| v":RA/ÑH,^6丩I G&/:<('B 1|RJ$9ٕ LO(o:#k%ccѪ>k/z )4gI H@_T-C-9/\)jv]k]j[[WV.u.TZj|\yeiu틩V}_b㾄i -m&kma;ՄS+ KAFi 5su~g룮/5=7lVOn}y=u[Oiۗoj1Ax'Z.P5_i%t[4œ=ozW%iIOH^ήXtaS*8$!hdh#ҧ_t-T ꊔ?q*b^#E*`ʊZDj-MZnBPyyMIUIJ;-G57^1 PP3Ib]7:ǥKiY(<@C!#VDQ&Hx p _;b ={â؈禧l1k5OJ=KjtrL)l@{˿<_}˿&}ܖh'??_awg>[g5=-K>·4iɷ(u>ڿXv\'kca h~ :_l$y-y ֱ@\ #2gy5r"yr}9}Dx [WH`5q;155}TbLT5L>ofѨL@7SHm64dnqDMTP_!57pMfa DPiщ"Ptf&f)N/ܱS`o #$>)9cY2B!{%(t{Q J)yQC58Pڀ3鵕݆?-$pȫ> %[RA}K*4O˷b+tݾ`WH 54)g` GO'z8=?P%8'|eD[w6 -;1bpմ6f2lte}B_^> )ΡTBw@Y\4q4 )ؒT糝|txA7kHlG0}<m'ȶn!/'^S2oB& y"knoz6\v@&PMF(9w4~С"KM++"()PXfPwLL,2"yc>CQ$FYq4VNxqtJ*WmMh%(>]K%kEMعN+o|߿Q\{=Uw¦#;&$ЕXdj9 MlAg<2ag'(95RѥѦ3L3 RϪN:W?XKd J nu>ǵ!=Y "`9s(L46?Nt^giU_L'.P:x:v?Zy<桴~r)ˇl– ߉6τ@[ iDfрZ"؉ AɁ B&+Q;$䙫 STlVX7Sqۑ\H,J̖3X$J)v #6Pcɳ(2ж<UV$!fㄛf|bdp\tqk}Ɋ|9go3)k<8Xukx8>rt]^0Ꙗe7uG1#X<-Eifݛ@_M4YqSy=ż iA RO+(龧Vש HLuGC'zhsNO0/}8Z#"LY)09&N^:^A#-A畐#ISHb9sy!o}x::^nPCx49)CΙ\ԷGDSN~ FIH75sBp*I_oH.< &C86qxM5g*# +͞ۥ_^m:)iI޷B0K kaKjҼF+u|wc_oӦMK5ym t})G-xԒnk^ `b04y`ŭ}_Jj ya*oY%@|n3!eN,n   1Ox.kꪮnw/ٿw:3 HRTYҏzy2nͮ&L֭&9X}iď +[^?Z$Z*K1QY-uВdcꛦ9| ]ҡ̸I;,V5;xEL-f!'$|K IIHT%#ɔ;6J(!<44WMsf҃?%@ ^3dNB e}ViHi:V.󚰑=Yy?d^$s DG^:U5٣=K?"Bu[HJ?tښAרdSrj$Uo;eu兢$3ыR$!d6L  sjz찒$\g;P|KFz@tL߉k42CLD̈I;ł٧?ä MUIiõQ c`>{&PX7BR|N'ʻ< CnM6_`..#,fFm==#6.Hap-4lvu6 D\@2RulEט,x?I "Ib\bPq.(i`|ǀOYZŎJ$>=tg*唂Yw.¯)? {&F8mtJr*7rU<>T Hw< X%'@$4dw}Cv_R=z1t̤t:$` Sn6VP[j:U4nnNO@C$ ^9rG[cɩK{/_YoTnɜ9Wm(/bq6 } s.C${U-{-񑉴+r j1oXd&tS&rƴHj0Ne#xQ"%bW=`>2?%tiآGIpm9I~aNBGM%m84# v+g d *\+:$E&gP{Q}mu* zmV,zF8?ֶd["S4 vs]D,&j΂FpytrsC~|,=SZ>]4mULs c!2d Qdr:,PGqZPhüliK%s-PcHR:̕.F7|ùi]aAja=MoygG O.$#'aPP3z;$pG1ʲl?=F5;-ecv">䡜@T-6#)}3hN'0n` 'u6Db*HBLDBAB CYނF)kVlxH5X &GnvhqB%\z^" gOϫ5sU"ܑdl8 E bTTqӟ!Fw0%h{_X@gnJҏ,.~T`| MSJrg-y 4*X(q6ˁNz"8im@Ɂu18MT'ZAsFǹoNwŒ'W=&z{:CC/LE\%@#9#tW&uz^)%)þ2^Hhw=v_}%kړvsh097>c &A| ǐ<xqlRf<0nՀ©]Ftb桑>>"W%Wvƕv鏲eViX5WDzKϯDVz}nvl t2|@@ök7=@Ҩ腒_.+:UA92{hem  Z7 ;=mrվMDtY1Hd`Gn-!;IjK X:6X\t#XW@<~ZR;AB)KcjR'$)t[Iw0oGŞ7Oic*?I@|gF0oov LŞvo:ߙ˂vOЏ{M ^OgF !拐m:Nނf z[_o+;ti>>'t̞vil!? +B/o:xQh OЏx7ٯ]v9|0&wxgBb!w >=u`KWV@Tq ;t6v,D[]`]l :zͿ(Urj-KpƓ@XBѶ(&j;dPjO)3/)dt29_y߻R@|x& %KBG 8~%g3$C;PD4KInpř_@9|jCOd]wLCq.%P:<\-A*Ch;52F5[BN<5i#H=^ٌM˵lRWFF ;n2Eղv زʍ tР+}go7> ɷV DKWm;mXad ~z < &Sˎ5L.xNaBnHN4I#ObE[c]Ӟ#A' J.m=EMN t'ʣEFdMZ2/Nav-]l2፳~U'}:ٌގ;_: P6h gŝE3#$$i1DRd,;O3݃44kQD@ood^dСBzS}"L  lE w-i-fRԀRg\xWtNG>wF`F3JiblVh b8P(8tZjvDl˖E?4^CG+ ss0vR{דU"v_lu:k .İc/a35=V\2S`xsW3Pazz -"$ɲ"?2cH-gA5Vp%B g M^Lz I'[Zϸ,꿨ՂW<-l5.^TS,K7"ͧ{M,o xc4Q"i2tuD0++11=\) ~A,w{3"8q#j't Ϫڡ)ELww^[ `V9wځQqa:Ug2;0 ܽA!^IwOW WU"J,ҌHPPPy/ a|> !'^v DϰN|7ރ#ݩ'^nÊ}H])!m[EL[.P1J⾤Ww|uLwp%Ci7>lQd0ܲKRC_kodsH>YHMuM a hhc6'>} n3HHϞ D~tKhwlI} ZsF$ >RKC+ 6ˎ%- Z3AP]uJk[,ƴaVfqgVa@D !:+n/dt1|32%|&9O bI"ϡ?vۥR cc(O6](i$#QbrۀGbsUOD~!.uIf@jS?h>^ζY $[-)h"$4V0hsù:{x W&#> ]v<Γ<6T+Bs;Aj7ef8ՆleRRy a [ H2{4Hllz1xm[2^ ̖qڷyG6r=-o> D'iS4$nz2K>PaþF!䲁m zFR^}t&R@7T7 C&;f 1w@A:$)DwDi/챦9 a%87dǒù'z|zA9_1!H]lNps[ƵַL^H^tJp[9oPM&?2QC!᚞:ϯ:*9y6ؗ" 47SqA,IN>}gw)fD,&?DaEq-<^'t]˄3]0l`D3*@ypRPy"AX7]Ubs0.= 7G6lgQA$:ﺌd}xRkۮ^_o$DToXK(~dn'KB4Q/# _cRC 9=gT?KnJ 9;R8f4P6΂[)0sYv5e>Vxq蜱#>P3)H_]imPL @F3& + 8Cp9bL\+DS hONM9kLMJArd`(z5/D +2SK}0{ o{3O{,pIFئIH A@i\\; Gbӯ PJ@p0\lF4} R]LlAI,m vBL !Ҧ#%O.葚I|"R0WEnDvMI JkkNA-FRD«e8%jO};ýX11M}11$yP^[)2N[l m7 (߸zTp/\٨düf%B%P]yH*ʗ7=o<ʉB+!LCQq_ +-Z jU'6_yk-hXm]^(SbݎLG`ggm9wwӹ5 "SK&hl[7xLU/"l0cEv_ y߃ODuCT3C=rn=Lw3XSUdk¨]+ʿg4v(hViө˛ bFxEf%H1D|4 rP#[d%zniF+ <9u@@$%˒D%koDKS;\&o&+lJ"n02 uC#['xy"W< bP6 P  z&Ӭc@dQ ?78IF"-H޳4BZȪ9=)'㶧a *~Dw{2.Q`B@ӊdh'qk捯6F/S)W2 b/~B<{ۜ@Ro7ur1j|b8'߅c2]oJHZ8=t-%[>r Z't^z×^=jiRI"lk+F kݔuPKaԦ?Y5u_WX2t[ sq攅tMsu&zNkmrOh @)-G4íJbp<n DEr؎e6һ_0rg%q ]P]"0%[v_C:éi$Z52kN/G~xI)p2Uiqs4O JQjS$N^F-f ~qh0G"RkF % e v\(`mE̟N[!ܯMM$(req÷ վ(Aq2 lb۲9q{uc*Nm>Eh "Yxl:K Uֿ(E}>׊B$O\YC$6D%TJ[^IoOg'6=,؍jKD4sh"7 1 jyewʘ>l:DB8ׅP{b` "?! %j,on .vN1QU 6Q>YSt|3+ӠCj&Sjn{֥!S9MHrhy\g7LY8B%}J4Omg5ʍ\()%oH*s]OI3g~(HĭF-4^vw ia׼ eƗ()Rt&M^681J9']u!7tu24Z0=&CiD$/С)FfEdd{g)[Db@!]~4r7^TGGiߟ4 w/tSͭނGjMm^?m R'C;+UfT$  ȑߜ %OV&@$2Y\F7f~|١bE>ZyDu7JI qp txy+y̤EG̜sp +=N6}sJtotΈ=vbmI59ȏH:H3 ?!ۿ+e*08-=V%\hf(~P9oјpl* VF$BwJ#a q, F* p:kr]2W3mA\N59˶v \t8&m}5 -a=ؤAttDaEljρ:aR骗> Kyj$=oGgGFRFaoO'1T$] pD9>ƶwx@c_K2ec/c yp"T/ c_Z/bTH i|9F/1H&BB%<)zwǍWyL ԿɏnƷLxF=J:Ӌ$ȸcbr/o Ѡh+zzo1%-f-Q9M}xIVFb5b_ 9*ڹ4LA%y19*>!C0^-rS3NM|J0؄f!FZ$,iGɊ+SBFGNbVLZAtI9H1}9ľ&?Qv1_ 2޾fN7s92N'>U."3/RC/GJ? ؘN qy}w'axXth1Sa]p^1g ȀHLr"k@fjӽ5B>Bn‰93R!QLy`$bj#0.bУn \D "?a5Շ_5;|I+Ik?^QXY1{{"UH,fPGH9:ڌӅMd ˏ6 Feiq 3MlbR$_~$nF`TBCVI4uۖ\b~,;uh#Dw^jvJŌ#'Rver4]x )Wոˠe†3e6f:)82m1`_uM Nr"du}_C{톚Y{2kMp${It H=C y9 %Ji}CfP5 X;"׽TiЂ >PbCF{].A LQvDDHT0")[DK֫J$(!fXǥ+A d}Q[ 64 6͒u25UV}$ tf2[f9Ú&E6m/{ D*G׺n`Sbֲ@N6 UEO+DE)WH?oPMĉ@yXGAG^PzI;3FT1-[$Sd- xeWl Vwyo xIc,;׵6['&cWLC)>b6H 2:?%Xd@0x'1f@5'~܈@q㋽diuLA$L Zu l,nsZy`iwe\RO.Ft^9iٝ-'aV҄p &Z@]ۻ֠7Cr°Żآwn'K }{>3]x~a;%UkM;pz&}\`~Hbs0ˏ9}|R'yـQ=T:[OӃ?D:v3#C- pZ3.%MgYzMEʯGf4rd%-7bg%k9׭Fl()0gd5 ;+*IQҧ`f?Iu>%R1mzb[MO} ݇%o(=nT$kό3Rݤy' -_uɠ(ub谹TehO,' wu_d;?;W쮍0:cXzeHQP*ťoDkZF *'/q8pB͟/1#.ظw%J?9OnR,VrÄ5J:i.\>rʦ_ylH;[?.wJ͆S&F-)ye#5W4j:36 !gH!g횏EgE6UZؤu%P/vxB2k<7Ud-4|9\.hy|q ٮ4kJWW'h 1`2E[0`s) CPKm>k[7k#0W ]y= m?m}v3V *nG3ggHes!,?x]̹ӆ*]J3I 7Fa2c D !]3Z)EfƉɗOBN d0eÈ5 OK1@WAs;dq?ofHeH֟YҢ1c=:mduHcrv6p/g-`"j t0JH RzFm|эk/0 tVF aa"gM+d$5it T?%.@:e7mƖ8Ge8uJz6_WmeDf͔?I(.$nkC}Zޫ}&l{P޸mk{g([fԓ}'F*慟TwRdߎCd_kF?I +>ͰcLS#5!3piFȴ?}*&׆(,Bz:ydL IB9AgI1o{^y%4TD.sZ THdgyZK ^v݇Y +Mm˄d[k`jS wkVR)@RtMd;W(Y䤐W UkP PpI"wX$#,W&k_Rc)첹:KT7}<2 1 65w6g1=t7)jgNz+l]D Rrw \ X!R_[KP&<1AB%( _ZK6a#0AkI:wp mj=F\ <ܩO>iQ)y+__CNqhpy~Z`UUȔX,jǷ} WPcKӃ_=0JEQX|<mA*d򚜚sN^7!*IęeJ J6Ve<jK]d{ZvwpS&׼LJ|8B#~mj",]A䷧mX>̋X `R`#];w@Q]jqp0 H1bb"/fVk] .1|\2bc(!>/PۭY(YV8ܔ۹ h'uO-5'c "?">pbU%ٛ]DKީ2=K:h rgdJn6q)8rws@HDnh|fL>-Dޙ~ő]ٞouiRI I@ !@$ V$Fdĉ3쳷OHFpV j Z6[9bӣ@ Ŏ֐cߎʼKZq$/,yd8ss!,Y@n:>&|h悹cF-p*ykV2uWtem wMGk` *ڣrȧ;+#(XL/s˦%/\]W㡭wDI͹/P"ypg`%# ,pԚ)@nN^y.U^q#=5L91{!cUg d-2be]e-̝}jjZrm@U>_}j'hږ>k:JM/t;kYrN33d)J_YHfT]~cƯ%8)KiJJc*&z팕nй2ؿQL&2ciFgzK34x}24N R޴srIoJ1ӵm9d5p:D{һݠ4i]SMHYճqKwUp:eIkoO9h60uS,uu>:{CC[^Âfa x#yt鶟c(%]stir?C[DD 8@:%eFS|vW1ǟ1CKgՅ>O s \m2}<YƓ!Ϳ`ݕ.wHš [jy5Q68ٲeȩ-XGx2.&-@\2C|zaG=a^ѰL 6^幬iV/Ȝj\34tۿ&#ƒX ׸̷&i+&t́l/#|`M˨$ZLdxɴ$)[R7bDQ5& ˚̣ok,F<YJakFG!M&3OX6."ޢ K[۸fET6aZXp?%I9b++%Q;d:]sjWc*0+gNaCOZg纁xQ Pà v?Ѡ9#}œxG ,x,{FYvBaE9>8]I~P^?ЎYWZd(FI`.q0qİbDUۀ^M ce0A޸h[`̕9#fg<8f8nAӮ?[2lA:e]LNŔs! /vAuj46MHJXKCTA.+.bиFܸ&#Hy`"Dj<0&:﯏SG*lx lQc;(*;6u d˗V ρb$ T#kO Mȅ mE=JN-K8Wʻ H qc|wXn=8eL@-IN]12<@qsBVZlskE~eK!ϥ.`$%L?/QpKZ0آ{`JVnkFaA&#+:śoά9M65/OwKfd6M@+?bB=i U`=:29> cb*{`4\R`.OޓYä4`2ʧ85>XMpbC`?33 O=O}EcgoBց xѫZg rPM3PFcwc= 4CT֤NY:_~\6(.#IRHƜhws2Ix4dt 2.0PbgNq!, Ӡ]K][b>_([SI ,yC̞rj Z(4X264;E+(#!H0 A|v&\+S='` L71\&),tAɯu&yGFC Ր>uhY]ļBKR{9 {lVBn8&]WԈhy=:~a̳DoZ2$A4-bO-ys}R@!ڣqmmk:Zo_l_γ~Fcxb -C[6GiඉpvM'uU{d%@=߮=@RB 4,( lH"pvM6k\`8  ,@9:H/,b(7W>'K]PoBf^롣P:XHB7@`Ls2w.Y@^@y&16XPX < ZSqOAɁČ(VӮ0X`ΙQD C %Tt `h:%Ll+/TCb!5?D<3ւXWO *ѡLW^N#%:@ki'RB6)X RT퉔 ,KV Ȳ =X{'42&t9ײvV73F~ "iH,A9SS⬋f[rwsӒB"O۟7&$0J9q!rd6/tU] B6ǹrqPK8ڋ~7bNߦuu䬼HUG`1{(󚡲zUsv$d4 ĸ52j`MI USqd/7?ĈٱR9m&5T6e֜C̢b6F;NQ:4h٬DIYrG Ϧp#ACOS47ƳU,SP%2b b KUr|"_h68%,[ҶxԵ{dh)쫫P: {TefJc8DuZg:u$2> M2P_hYΦyoqlw'HzՌt_2GXpB)Hqh_hik-R!;AA?_T V/c.OmλTk{d93U m\0' ֩CMd_{!%:2= >>csBgS<%cEu~9`>!:{3{`E누;/"T yHnDΒ( dHF_:PRt4 /Vd%wMO5 ZWoCZq-z "O q@Pn )-zG<:7~ySۺ='<had݀LyD2𽎡!^ rwӹ3}P ;>0zݬ9bGДM"z-v?co9ciëDd+% .N݆ mSx@z&4p"'˦{RN{Csj5k&ᆼd; n/&y'ĸ-2X/9(}R~0||'UsK%`FM@i:>vO L(h3g:l /iؖG W-ڠs8I+)FNhk咑b%̦rĞ<倛hjuv15DhH,݀,֭-x[EWY# (3b:Od.IvHfX'j ΌĪwbۍ`"55<;ttпnNƬJ K204 6 逸PD3Ҽ?H8Yw3D3x0cv9;7Ƣ"1u< 4聥F\T|AcA@2=ftC=}.)3!п,˛B_;kf w1<;nFnּD{qpNI !<Բ dOL=imIRVtdƊ)aӻ*9+KQ&Xf>nSrC`l]}H U' )y- ecd;'b֟{ࢫДNfaO#DDAw]B3NYѫ4LN,>f7\TR"b~eh4$աC1Ir^ú]g{,ݎiOřˌ;;koN +6Ո/Y2юAPF̢Eʔ})RS B%Z32&""p4ќQU.$;E. QJ"k&؆ D35Q7`s7t [%|vD4\>xCL<lvX*s yO\;ͧFQC5F`+pSyiW,cz,MPd' u+GmTӐZN.&c%Pr6tw)Unքe,6[jwaD$:NzyI<>N{V;^7UIM'lk v aWL /詃?.%JQǽcLF P!u][y@#<=^P.9AIb2ǹb5:߹!wB8wEWZ1mM27xV^$Ox׼f"Ϯz@95>ۘH,xB=ҠSp04Ihʢ:ŦB#@*"MDEh]hYc GCʼh%&OZ "_b P NX&Hb E1PF|dd@dG%4c5(vϡj` mHwЊ{T0.r1+cirÑd:0,tzjHR2lg4ϩZ!Iq"tu}%RimrQ1\Է5:3{R߳ŻBy-{gIw{gsG|>2VD§X#"q'Oz+[2ni ؤͦ䄵X@R^K! p:A أÿo: prW 18ڼٓʕ ADZ 1 C%[5 !o ڱՉyz{l8 c(d|6tĹAOOw7SWt_c1> .}y$$װT y*rYsX I+Ba#f(J+c1@k _ħ m7&~(d/y$^ ҆Ă+Rl}Lp].o(,S/d3bsjraiƗd5coho%.U݋|_lڟV_V~|7kZ-^V/VlE-w/*]U+o?jwU^MX߿hڳ:4+h^v_ޯ4~674xhe64iM-޴6tj^&כi덿=M4*-Nt\˷tD}5oiS௛>|L:$=%?rg[~̘QۣPC*z0{Ni6:G?+z5ɣC֘%aK 3FvELG`o,xu !<{rIh +E-vGca~]C>dz_pѶ-}d@4,Ed63W&dȈ`s74 ry+yO; Uġ^@IṬuc bgmn   E8D(2("/Sņ ј73TxHe`5Bhw iw!89hOduO蓾]j$QRqR~ª(x{@I3WS֑N*> Pݦ8 @bE ?|7^ՑԈn6޾9`&OJ/@?F"O\@?tE}3D!+:Z'ilfո9U'E[JQy +c 3E_'腂 dF|( B^t8$j I~뉎L9[g(c>tX/jw4"ptznyBV;,c8׿e}<;=uhsF*ѝޙ^p%h<Ixur, 2 c"HRW s augnu@q E 2.GA:=Z+3BoZLzFg$M&= =jI cgr_t/,v|B\,($0ԥD2nԎHYR/BBchyJHG])T~0QJ^l#3H#%JχLH{{7}^8n&ehA0?hȘ=Ť,![ 5#DZ8Gfd%1`f`03уG<|Ukx#ۼH̡_d$1Y+㺻 hw5Х^t6k7NBK:R?L䳭E,KRS>u1̦eAvZ} 3ROǨizِ|<2ǜ\r\Y'GBuNgܽ)pkUOnCG<ŠB'[(uAw¡Za}%WoRث}[-j{mrD܅/)hJ?{ФX\;WkS]wR)th۳=ZG~=5<-jvsvƠ5Z:^j&mVIbm@hg6* 5mm+1k}Ղ9'0r"Kb\ 2vmo_>a>/ W`sz0e;0=gE@A{Ӛ.0c3:爓iWIƀxVȵ' )UؔΔ/x%J$ MںW 埾b]YSnH"`*WVcbҊ9\UA UoMT@f>Q‰b%&q'_xNVO!^-" iБģdH$nG$6gm:ÕhxRE/C</X',teB޴zI qup@۱۫>ʑ~|UWΏoVK~"v4u|areOW~|սů,gkj6}8j7W|͓e>߿٪g6a=߾hfkMZSժ7bJKUʌv޴BNȝ*~$==/^쟞+7Ilg3{8ݩ_ܙzsRnL9{SwY+-Ojem?˿}0?:6ysI+@Z%?I3iOf8~0hFm͵ҿW.}y~fnX:+VV_1ZS[i>+}&4Vkޘ&w׶[iWW{xnb4xezirQ1Ϳhh':8G_i]j-&ڃڊ+h?w[m>&3]@pW¬KINhMdZSwC"ևF5ilcvW] hae'Ad]&b?T 5/F۞ 3ehD} ߖa̻΢q$A2:#B<|tP8ߪ] EttSk]!DL}6޴8AHs NH Dp2 }"XL*C\#P>3 Hুh>phHlw4{l.>Rr*e)Ezú:I4FNTT?ɪ-5- pdQ4p9"}JvGh )z :ƖKA uU7"]}G`8ӓe' U&9BV"yL1`e@m@e9 X2j)`pDFd\t捥 d_<$s !W+zpօ)i=wikR έ(>\-R.E 5⃁Db E0DMϮ=c_=9!\{f3kdbmmߍ!  /#(`GhC+ G;C  "yw{  ' @q% ]G\B_GȚܴb8Eɵ(31 EdD7VGƇHLY*=ZR uIa܆yCtCuV k8VC%Ϳj't3A&aﭙ̀$I2p2BfHlBK4 -F3uruoX]dt$h3IM5$ҍ# "&~t!1]\繠շ{5(HW"WطMjf55lګk k۰ka5\km. Vus۫ :F&ϳmPN5@"5i$W{? ,rخI$iW"~38%C6(AEG`ԎY#m?7$e :NOUwzU_)8Mca-yjuad2rx$50uaBI 2lfMU$8 3MҠP ڠzF5e n]#5F]aحdFq ri$yqHKH;M+yd嗵븟хaAeˢ,eJGx1S Ǒ ']ݡQ>RX'@X9fq̖2[}8dIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/stackWindow/info-panel-2.png000066400000000000000000002450011342332007200250570ustar00rootroot00000000000000PNG  IHDR,M pHYs X =tIME/tEXtCommentCreated with GIMPWbKGDIiIDATx\VTɶ9cW H"}dH @-boaS}YVq^u^y}cX͌_.":~~=󳱥;㽛.mM^ܙtrA7վZl+3w/;yQ?)R\ޛ~|>ɋzzx2wl߃ZӇ(nn|F~U)z7{U2׵㺩*TBڭ* t妚lxMcūWrֳZĺ+y=5VwjJ?O9,^U)J%*_U@ӳu]GzCg׆ߺޣeeg,yetW[FM[UC%D"WqzM.)_E.]PwRM]Z 识DU5VUL$P\'~=s=+@N[}zK5ԃ^51UGy4j^Wz= &)=yl +V â~]`H ,D7*Q?#hciRYJ P98A]`dPm'%_hejYʡxX`tWBC]ފYzfk?ASХ+jSm+e j8ӌ Rqz/޶M'sa"] (H?6o0u+db`AO`Å*r`֦Il1vW?L^B'4EqCi6RL]ٝR7 $LܨO6eë Y/11|L2]ohE/* }{)f/ [ӹEI gHks^j<(7 c}PfS#l 㶊7LzU :J"y7Ͳ4z7,8#PL-K]yUʖ'' ,ҡGU^$nڴ LpEPEaFL;gSTi3(8K^B4@=JL %Uī2;2E)eE5#T*Hʼn,*Q1Dvz,!qsBnuK%iqD/G.}Su%su)YDN5Tӛ\m=N/o o31?7?;2?`? YRg" BYZwN>HWw1L\Z"8GU[Ge}1Sg:Zx|ǗRqwvNw>oՂa ˆyXˆR>˛IHy ޶lRge@8~&y䀫IءjsNST (&RjiC"9 PPd6d(%LD5^!EZG@J3=٨,OWtWʇk)կzbk.MC_I/{+˓ݞL`9y9ŇMsɝxkyB?Hi*+Q=:'ۿ=͔R*oU=l/,Ol-+ܹ5 ƥszPAEc§JFESR*[23 tiV1 V}ߊ+)Jιn.-yʕ3qT+P -.U%:"aۺU]Zqz(k>r9]"]0 =ta LDaTUwBmvxo |')ϛ kn,T7W{jiY"R﨧l䫚{=gD;aOh{rw .~Xz=4"f|1Df<`(YMަ.D;n@50Ћ\<6q!#S|X3G&N %{eo ?g2p VCgXv0My^)׆Ø1Ew0wɘ Dۺ J TPг]=ix8:dXs`npAaFpUu c 8MƋ:ŨVΧ%o%}_a֡$+K4ge'/fB+E2µ":)נr]v c#])uț90Ѕ3HbY12U/ixw_dnCX@GVFfOsVUŔsY,k%DHF`64ȠaBX6ʝኬ]36Ftߣu.^\mims 1; 01Q hߕ'cvH:jDƯ.2^j&kv&O3 cĺM(8wEJTĞP=&ipıVbWCc`(iՒZDHZ:z"(L]Xz/ dsMy407»Pc^."+`:9+%$=S"_h.(ь'!"?a͚BI%3Dw}βtg!})~V%$eY b00YY_EWAd(ӵh)D j6Ux @T"Xц^tL^@<;݀{pf=S=7= aWf RUDЧn[Y|X'p0<(ilxx0k(3|&Bb3d؁mĪ-ZMZ j рcM*Bh^82h6'-^7>:NtoƩUO71seu&;óR^:&,cgTU%4O+I)$J{^gVZtGNˌ +.i lQt1i)90͆! 7Y|^+RX@R'Z~ $JR *FOe>,@8 x&5޳=v; z%)XJYf^ߙ$"68E1gmX*R%ix_}Q|ԳFc|Z6r{^}!y"" w%RPfM%+aq1 KP$7Xxsdp ?;ޚЁ`cyZܴɹY6q-A 0!1hKf< IOҟܠ z+֫Щ]NtF9lĄt eXʈ]*6JxG>V|5 jbEQ 9fhDx@{ Qo j)t"Y]I%* h8j48CE㳌o桋VTa堺c&:U"\D8Г n_KOG[LEC\Z!%l&yeMӖB ,[* sTQА_f(pXgǢP@!(0 Qճ BMDl͔N$Pv*B6/ԯmV#TI 9$Y{rC ɵYkds9fq2`Z}4, D|f9 ^6ϽF#̶'E\ ;V0аskr7}>٬ica-/@J#W2#%^b)Q5ݿtsxR X4evCy7bx EFh; LsEU45@\f:hgF4Y*jxSaep 2Y,:(3jۜEΐ̜&+pa!([7Al-q:PlW"A`4PZ1N'.ya VBqHCd,GB%8$<~^ ME' cnVNjnP0XLnd^t<QEN9qL8/ۈ# K=Ǯ=Y,!dx|gvd/"[YӒ(>ɹdI✿e6G KQ8b,AKG˛ȄWB@aPsP!vo vH >Ѓp/6Gv$ݞ=Z>#+V tP7&ޘG@\2`!gerqe7yuH $ T %i4@gHL4g"K<}YAz $} Ĕ4  NL&gđFu(!'G^Cj`TVHg!Xbcߠf`;0ޅ9 &SXES)[{EfgmaGH`O:jbBb0/b%(:Vdxl, l_QK,Yq_+!Ab)JZ9z=DSݒKH3#b&s&Hr~AP*n!Ke2+}ëݭ[z5^bh'!!ΣrᛊpѼ|͔GB؜[6cQzacJHF:~Z `: +dX$*gxsC/=&¦ pHTر=/bJ%<=^̀є ê~¶aʞggݻ]6dyq" +2#'Ӹ{NjQlJH9;6kz={`V@, cZ7>hePUYy ~RbCk^52,Ê]U˜`_qj0jRmlw D*֬\K- VZDl3_]-:̻SpA]jp$qiP˘uD>:m`Zٲ&KmD G, 3NDYbf"zm7-0p+ɃIRhJ*7O]74pK%|dž1*$RV,a5eg{dt}=M gO$Wc\ &zvg[Oq[X7أi2Fx*hbcȱe,>dz+ia`5k@2ٚA^HPMUqΕ=}$-KϮ=Ic{aUl$ѼG@Z= yLBD-xDkeq $ZoZq|5kLoz}oĩ*IBcEfk+?H(vrU% X0ƺ{ٷ~-XӎeRPVLc@Bh(̐e!QlSori䕈t;iy zcݰN`l AN Zm/e 1Pα7[@x輴l/d'YЌӂ!>P"=3IdS (>2A#(p.,l h>kۼV:Qc.;k~l>Ah}^D0ua1*9D&ׅu4"U(ָ8~7DڅTY.[h[X0k,G$3D`AX(##ѣ9~{ &"=m/& \fK6$q=KZ; axo߆-t\*e u,DC T:A܏+WX%.s<[nI  #K& :JH f2P&ؚS P]j .>^+#|ܬXGf۞h;O" K ݡ~LKAsRcYu˫! PmհkQʿU`{2TB(hh⏼3ۭZolP Q`u D B暔\li4oȢ5(vCVކBJe9W^eEǃ) ~lJEKt!fiw3!$@‹͘ZOvFLbMnm!Xe O5q'tpwbu1Is0&5 # @YӍ߳ZbߵP4\ql67p QbJ={Ց oV$qG->c,9Z8dÍ8CmQ-|y]#Uo\<,MeV•W A> ;2~.6 ˽%qtVv(n/A}fo ĵ#!iA3(APtJLhKi2(y|$(jx WI$!hcv/Xzecc7WKr@Pʞ8y/@!,{9jl zʚ@{AWO~UBㅼ$:/wv$ռB> YR9De:L=VGVۃGdwn~9|8l;;HE/Y0bG$2 eVm61C3(I,"{擋Fecœ b宯G"vf/BYÞX/3v8~9*č98Ҏ+!3T "xp(U+QN6 %q+"0oS,~=$`}ҁzڋA%JcP wD @$H}P D$1ȌX*Ȫ_|m|w^(l@%p KBhڒ>7sC4#(+ O]6O˧Jd-U|Jt5$_|hr|w~xROzw?99yC$ćˇN$ұ>E3 >-ZB,tFK|H}9@~nWE==t<%;R'R/>S6F L8'",lz}O©?5|C^?oNq(q wQt^o-nM^pzš~1]UDtiEqx ve$Pي3Yg)˒bX>8>ov'ωw`cוK;|yjbSTdߔ4p)xk湏af`|=Rr}>Up=RpPc|@j4)֐Jޠ_{g|5!m\qGSڞQV*8Bw' (ÎV4_ޝQz ҽWS>['*nkiv5s'ȵ]C8sέ;ckc/]c<(%+%"p"Gn'>WNQY c.4K *}g9ċ ^|o΄L U@QԄ>^B)G(@QLl3>WC?)r$6DzzMk~QJdtɴE)Uy5DWesb&Jzd6tݞ'oݑHAAxŵ G) *6{Xp敾CRC["<6oyV9 d;Zq,FB UVu :P]ҀwTg1܇v,ȃJl>ˎsl4ׅɖd:ẑsVCeXX rH,>%i}O\Tj+;۶Yogp/ΏŽN;X> q*4q8|$,~-N/oR*IADv}s0,'zG5Mud+J:/Lb[D|ƹw|jvxYJ}~Mfm^gfg/ r@k=Iq*U !>س ҫ7dKb8wI^-v\C+-8=*e/u2x{rQkfTs =%ddo-CG*'uU,q! )f;\ȣ ~fy~%,[0''r^kJJ>:$nIҾ ,6eCg4<0r,Bs'!R* ]Tsb%5;f{=@f̠ЇAGT"6n.A=LC$J6Z F#$*'oy||c, Azl@cg0 |:#H)W9'g]/d8Y LeLNׯe/> ZՀS_'n}\D_>G 7٥/ȫƊ*VPm*Q^TSCq!'ӊزv> 8+VM=Sq 6K|N|A)V~u(/ |$${ hM {a"R9/kJ99/gF/<lQ1b%o5;O+K M|x·aK91Gabp(Z'̩tD=2xJ]۫c|,9^ʙ:3 4aU6wfO9V>Ie,{zAPű0XG'nr&`pfE)\6H➾9=H"Dy55*+a;6,;S*eJy %R$8@@q8TR)r̪.j_g{{~/⊅ ܈ Na7ks3'DH Jk<0"B~:lxj (Cao[Mps7N`[LFi$/e2F"&inHjyD>\:WApɋFOufQ 3}#ЬJ@a![ljvc:@xI
K j~5'!"Ka)^j͓+a(뤍/GЬ3I?Y#Y#VC')n#.A7'AdO ;}@π%nV)7F iT$4ՠ/wa2* :LXV!<2!QNe4xrG3}Pб);1 ,cZ;+: ,x(jl@#kv3X>}3{G;yS2!|:q.҈3kn-47~frozl<nאb2>e%g uTVE> n0;+Ƨ=¸S~y}EĮ&OZwA!$"xx\1lRFen(ſ3AZWi+36 h ۝/}8?ܿ:ɬh{Mkubmvݴ|[4@Gӿ-WpFLz8|;oԟk}2zKebz4M|5EG`֎W4[[є>/7'3Ԣk_rgڄ]r}R7Yxrp-ڨ1asMɿk}7r}2-[XV w3M9[9v:7;ګ+}tAU;^.h˕KiFAٚ+ުkSvN] h]cfj]5Iu;(î]NowSlP[kf:fvQZ]d|^mYjeh]%M> > zR8sE\С]Ghzudp WK+iwna)8sвhGw[̙1__묉:*iLqg^W,]@{Ѻ ]k; YDLRz=ֺpuT.jsgSZ,j+ZnmLôrT܉~t:pҖ񣭵#ivv`=-G\63`:.իS'M,zsДDZԓ墑]m}EgGۏw?}vov׻ݿp/WiˏwhUӲo_.;~G+Î{{EG'{ՎZѢh&o7Ot|M4w-hcjVHH>>ơ][K#dgo~vؔglbIy>Yxd{>^ؙwrnd'KolE[^Oo}9ּvA{ A;ݵ/tgS &:Vl-zH Ƈsl4O#I$.si<ژ[Y4f:iE]y6 5ౝWi&szc]#O.NM:{j.BUՅҥӿzեf4s}vu[;X' _[.Wkk_nԟֵfvتvgZ[ݬ'ehܮ]r-J/ڨWmy|;JF:Nb5ml~wa_/cj?Jthɇ}mn3k`}GӀW6Y^+q/ym+/m%`*7m1^>4\O;c1qpT`q0Cd$ͶWW M`4YP+`$, s)ԭ:,UfRTWn{/T D!!6C- BQh>'Hx8cRs<N2H$ ,{S0Sɡ:IRO?O$G\+ #FإW+S2,d~WN16//u?[:xuuuWHzw7#2C+7hWA3\^tisC]cW]餶]kuŎm.ukKiEg{-۲j޶ hkhsDu;KvRQ h!ժy4Q jrb F~)+q-bAljA+,.]&}2&Ae٥h2ڿ|$IL; cyԩݓ~%%LfehP 5I+v (wcj+2V:7&z&\ߨ|}sOCFvScA'=!$I'\aW4$M9l! LwL%%Yf =w:3'ID3+3Ϙޢz rju"/1qk J4cL L1gAUII .834|Ť,t\(&e E+u귧_8 d"v> RѨBTd&m,[\`Ҟ" 9 GSԬ >0d W5FKU,&,^, kDDn߀U6 `ʈ#tp} JPXZ+Zj#n( Xl9.l1wҲf@\f;`e|N9eM Y`!cj7[G &]7H ȔY@uzݐ(È/t&e#Բg `1vܱ(aȂ5YO<pM&y£+AKN&-N=, ᲂg9cj_skKٓlC+qYA/A33qXĎe23HwM[Fu˦"93^tYZ);ko#ACPg&X`[5%*  '%)Pΐde0C-_)kr2ڮ(x3 ZJQ[.I=VtY.OOe2)?KSjw2@D/3]P&r1h6vdL#T͉R*'l ,?KB-z[+_?ۇC-2ڋuWorQEԗ/כS:鯬pFgήi kVF nvDi6>g݄ǰ5k0 TVa--g6ܪi %6aasdKH8Ka2d!+Jq'#N 7G~ua$dn0ow=Z3yeoٵуil PM )Tgh}~JFƶ =aN٨u_< s{ڲQ ceM2 a ѨPYjV꫗ άÐt<3|e^d0h\2s4c^:dΡY&!Z *J?6QN47쪳F D^li)tyH ȧ/,Q8}H9g/""تrʬeYLuF54h]Mbnה!/7PMHV0 hs-3UG)1/ C91:tөP$qu,5s#jya:<t'т);V,ai`áYre S3O)w$A=/h`xtS79Mm< eֆx3i?te1]wv 41ht%d EԺbP7>>$ŧ2= `ZDmٕ@Spr)$iP&r{Jg\1LhT'`M+3:P[hi w܉W 2D+<;pSw;0n\%(TOJv`:pSf53<“X6^*% \Ɋf;C`܆{ 册mۥFFq"q` '"3㤗hYK&rՃFnt·n s%M9SYz1 ϶IũqHmn0(+R2پ,'zwڨ$e(C.1O2?;$t"7ysOw2+E sf7uI$\G2 /F@N !sW9ر}ȯJ$Oc@Ej:v`{024͚:ڲqҺL̊lgT2h$YBTB.Z5USjBZhfޜspD 1OW+];C¬=)l^6oɒU DלbKCI`=ij5H`NL=7[~(5bjLW n 8aUF ԊҦw(Gt߰Jl1k1r]rKe/gS/5J;o8HMwIPBafܥ*&__ B.\2^"m&8 v! N~u)mÔS3ҿtѿXjLXhS9ËIRu'_ؠ~~tjƛ Xr˖Rb~79~"GZ!e 'Dݥ M 0&ZO$v1/XT{ j۠e֯crYI]ϡ\qؖZS]'^Uݧ+eP8$V8hnL`1i0Ob0E}ltf[?SZLϥ%'aƍ1^0X4%]3WeRmT] bXLC" Y(}79\\\TsY#9A  mh`(Hd8JEhcs m@8[&Vgsso[b5ϖ bIlXn)x8 J*ڹWH3vR8ZX8IqBmc-b .nh!Ch:PהIpt:"τI?T/Z)C6 IJt^(J3udd|t Gv3dpӿ#/S GPJXi:X/UwkiV݆ceӠ=oD \ᢽ-#|%H(MA6 QM&5ben!- ƽknС5}=c~qQlDDWB3 !үz}1P[ADY@^h!*yI(eA\SڤE^Y#D.񘀍R?̙[ψ@>a>i >ʒL1Gځ&I,^! !9@GΑCJ@NC$IVZ1F.hdv[uתdd)j`5bv2LAY*;q4lVm\W"يET/fM5'BJJkg(_2i%cMNY ؕ(K4 Ճ"+Hunn>5ƓW.:4!~eH AW^1 qznH<!:^Nhtp ;{,`1جӤ\4`c6m);B"hi!GHu Ud>@SM}q2/,qWherAJHYq[u82i ~b`cf!نIsC@ݘ) 1E;FYMZeb l{z@ XgafzE?X9z}ĶtYj)MfWֻn}P$ct|gr|VW/@dFHh LH¦i;iqw[+ 1'x z_&4FB 8?"4 RA|"nH/RG'QoHf\-JsK;Y`)`]mvs KTt:|iF$z0 *"+r}2[DGzWJ:PH46r* DG#ݩ rx„x04AE,絉g; ^/OV+#ěCNC*l6bzJʬ̟L'XS &`FJfexJ]26Ĺ=_re 22}]=NV*YD~qJ5P!fPIP>&}M$(d5 Lvc{ EB#M2ZH}:Hlj {Iݥ%TijoȰu@pQ gwҝ)Z4.Uɨ 5wd2I…!Ίސ:&NXywh}3XLaA;Xv 7W(s=(LXU`Fƈg$A(PTmVfe@<'94sy \@iR9jx`Ek={t%/JfO'{wQ 5oM|0k"i\5|Qezɒ^5>4YCdTx8qZ2薥xc5G;+Gm|Hn79d>gϙ=e&=K<(1svLXՕ( ;9vE%Om!-g"5o+aq{{ >)Ӧn?Xh*aN@_t~!ӓNb[wha z3v:'YUd^O^$ &h*Ɏ[܊cUȀc#G܈#*/cv4ci~^z}7]gV熺wVR*7P܏Aօ܀M惉|_H,Y@LNb0VDc#hEkF^f(vWGZ m0u wLC@XW;))\{i!3H̋\;$OU,8E K{(Sԉgm첾^`rޮۨ#$)vY4   aȁ4&ř~pNőwَ' O]̪NC;y Rj}OgLL-jZіId"}#֙'3ؖkR,|:L ֐POEkzI͂֜ЛV1;KŎ&/bZ0%E.UwJKDls A r.Z sME,`"DB\  K&Η[FL)dߺ 6ReZX [elx3$I[זC`Y` 4FgYٰtmSzXڮj\H5UM@G'@`NqӎU?X0Miy* N0<̴P3`T$1@\xŶ/B:׉ؾ:p5/7]& #&} kIs^ 4jE,I9x\">xڥ>knfGngDaңfz5>@`aE@ C^WHyp;2hlU2I#־=h)5ASX&$| YI>õ'IaO ~Vgso6's\vT(>7 LIkpZA, f #m>Io )k3vM1O\ዦ=19]b4 'w8'$/H._E%8D#YepjƱ-e_: C8wO U;kӕzz<)j<ծ1[DZwǎOל vɰw-)T?xm"AD)a2e~;.@}3dOrn$ C^ƅU 7)ӎ]taxJ!XHWLS<]q t7J 4r.^L5 *4)8hd]GC>܌\!=J.Yg*? HEǐvw~D`)9>ÒN";"q ֲǾFzJް`$ S}>ɖ>e0To,yĭ:Hѓym6QgU5 /ճYTsQH$N&3QA]mV[uf{^4li/_4=J |I$_Li;XB8בzΖ̥'Z3LAbSgM1֍Q}=ܑ)=י]!='-W;ɺݖ1ߞC8v 8r>Ք]O~d/vZ{qG!3үEҡzaMxg\sp$.Acq۔Ł~0k"NmHFKT/蹠A@mpSF^; B;{kYڍo̞s3%a3. t*cFMTCv L9Ͻ͔̍^B"訐^9!E1B6BQHm\߈W)F-_6{=9(LP§yf謹1)/R.,JI D1YP|{i{_؅1m9k5 ~G SXK}2(hBB^PF\&rưlƎځM۲h<- L9rZ=S"IiY21?" &_/%=$aypw2GZ38@e7uC _>M~Ы/@saiώ oys" z6"w9p!)9=R%WH @R[@VӇ~9l2&VHkstЪ^1K(X{й̋ ~'6Oz1qsc!r-qzd ;RVP2(x9" ;s2]]*.&iİ/X qssDK֣=mbUR ʈ"օ]2Qil\F JD&+xg)u' CM i19gaP+}58X,Ʌ7d$!9tUvA[ ĸXo8UkĬ-SqHf5]zˁi)4)V=qYB t:hzHJ>K^z Gn$p;Yȍn;hpZ^{=F͚3D@^o0y(荥YSa&>ݜ񁂁 Z|~uBDʲ yb4Њ7{"p_:14(ֈY(bؾ#{ٽKfF,:C۵|{ITfzЄxڵ3gvWA1r-l'S?%wk芿jDЀ3|NID73fLAuDXpFv0h(3xרsbלԍHq;!\ªw@.C53L<7qǼOܙӮuo6+wk6kn%s{O#Nn:ciZ0ޞjS]YvQF Q_mgr ٞSVsl>q/re[eږ1uugyHm 8e p3%O:[lUbd˦\hp#\dݬ,[ :KVP׾pga;WtZ7 K=o=T[h0G.RWn.k?OCUWAE#?L$=C.m/0Vٹ0b#[/DϞuٛ:J %cu'%G 6 I YOn̦c67gv A\md$O$H@UJƈAQAx&c54*}5Ú-vfw]@o*Ia 9:nH GQѾ5=q,l ZnfYL%[lFY3K@3`kJ4{X.h}Qr &-ie\5x"<ўg nSZbȋ=< ۏ`GWyaвY'b {o;B9:a/dZ edmy M6i"& "*HVOv #VCv6lWSi!^MP#- `.pR`Kuy`8 U/E #Pt";pc $1!JZ=t-K@FE?'O/]jzxױ0MFS_#pll2m 0 =Av+^]Q Y;ׄH@s乱I Ym15kel20mPP aR'do =BFK9H<4ݙgnz&P0aBˤdn-c T1el; (|_#qra^R@ R ЎIܰ;vV֓x _ q]}|~ޡpڳx3WN#.QiN?? HQ’zl"1t0eyg<[7bvxk z^+4֌jlS^9o4HDH=սdz4T 9Gl'zQ$'% ;k.|S ]؛h٫S:G[dIds my&a N̢Sd3Bui3^'VV6x{D!VR3Ѿ%Bue+ZMٔk=oV,Yazc wqRߓP#=o9UZkPsS1$5@B]O%l `D-:3|Cۜ~M[[&Ft .0`h^R^QӇYu0iYCTŜi\ <F9MuuKV)DMDBOB\lDE"b`i} 'e8#k YvH$sˈ& Fa9Ѯ E2ͻ!oo<1eO{Ʉi7e$yֳ}6 'Yr8EHVkñ]Y[ێ\_1F R–1[M_HԲ]:I/l4ȶEC^kI"oVi!GͫjC4M:4 .OYw E?_HqaTR$A$^6Tם%0*hn1OfFr΅xt?=x ˰:5 #΅\u_Ńͬ҆5S*ld*O4zG@cţˌS%R4MrmhW*R4\Sփቩ= FEN~Zq=nlrt`#&@$V'M#Ƣ,xrC:满 tN}?bʓЍ =dE~8HX^2K([ :j+mj0S{(a2Q-LjXA$X$K-AzpnRn(""e!6p̀I|"aŦƳc gkt8/HҐ6Pݓ)3m^W0[f1@ZO#/mH5EƄ$창(@F+R[V&C6kҵ]v)x6&%hL8L$ j).{)efKV.{7qXq‚NۼM`cKn:s{Ӆ+:ÕS#σNa-H}8/t7`cr}HRH M8eSўdsei<[SHAp u-!Dg [ Eݡ *1PR˗r=Z7, aV;Dg׫#4J!>V0D6)D~)DX56N>6¡b3 4a]ͅMR.D]Mj{ CNI:] WfDz[פrssP I&/CӅ'%҉ U!SrݲU\c٪ DcQšS!CA]QɉaĦuEYrx\BHOېwZbpY? 'V&ڜIՊ VL>vOl_nz 1|8M&Plh\#>ܬq on&6UвgvH)>/Hb(aaȊD*o,\<ROK6 XMfyk4ҰnXP`CJ!$^lv#8gf)9؉V>)$0$؞?٨{z"#JHJڏ.~2A ~`?=!ҀGDC֥ ׸E:01f 7ߎ;xzr>*F x{oj \C5ٴ0eMH s-o#+!;kw8  ּՄNk0WbE Z'xJ2i6 ݇o4-I}>0gͫFX$< "7B2_<ȯгᄵDF h&SɎf"P]# "0^ިqzŦ\[X,Vz ٲiY mR:Ҟd,_ ApݹfY' CÚ!À>`\sЮ^6;g c {!;- %O ¦MHŤ:n,DHD]n% L\djZؤ 6E'=w;{h6L^Ury3˟ .'[Rl]ɡ7hvujku]7m<%3p+/s$ALsm隃= YGşƅ>rV\ێO[|REjϗ "dQ9 QԱ3dzSRc@$" ΄<}i8A7iBP{ЛYV\y7r">EfVjPthwa%4Fs桷|6do*̵"HJ=1eo SNz! h3``|SwRUtJKj1ĵj[[€Z?rhuoSJ\ [FޓϡѾſ挰VtuóYm7tIJt)w jl7=uvJQk"7pqU`.#aK=#2\ ?lұ#U7ۍ*3o!)D~EF[/8~6G\8>5i8XoAuz%izB a95Yw@F4$c⤗ىW/;;w6G2?Ŕ͚q!2IQ( 8X/eyCIԖs0Lq(Kdϐ]bEOtB- Me/1$*ҁ~F;Mb>5O],Ãzwh'Bab"3Drs8Ŏ.#+m0; @~+='ζ }8qr)+0_{Q2v*񲓢d9@ 93IC)D~)DRg}үp 5+ hxDK!yZ۝_;ϐtl,]w񄃼[ 9., ѕ03%?;yẂ`70ₓ/ZbH,V2&ۛdn j,UpRf܎N͌DGb\1$ftBE),ga.O9E/XFA 2lއ{QBo+hGվ~k2N3<`Ѳe)] 'lX9I"/{s _XDpgǐ?pz)D+aߨ?n?8lN|<1qܾzoDyy1]KNj3Z z _Lc1=31x-Q K//;5uѵ'ijͼ/B`ԦkYMڭ8 Bɨ[I^LI'_b1ۆ9g1fX'΅^nm!=Y#Xr黝3\!tVcZRDrx'1*>*#}2^Kiw/ 3d**^4# +l7m?Zۤ¬_~y0}~~zsnIIa'QOOPjǃlCV9>OMnyV[Zz~2rwiG`~Ӗ燭>lu ~j| 6O"6T-~:ʯ6mum?6˦/\7|"e0\?wjh|X:55_rLH{к֫9d}W='A˧\Ȧe<)AdJ3ɍE!.zu Ăڒw鹮]ʢr\ـ 9LE[a nup=c`Fw V7Yu{4\ǼkyלWvhbfu1L׳`y-kcTF9.ʞ hos" AɟN[w:S(ʣ͹V*XMh'ŗel$O3cjvnYӋNwC~z)_VE~]9.>ޚ;X/h&oum2hv~jMG3zt8oւVtGޯ|k۷7*?߭nFqc^x}jsᝦ7+ou: [Y{ ?X޴MGyДj): ~vM0h9/mh7R DbqxI49ƋNUDAo"YQ^w!n@܇@$gruWJ9.JvJ[hΩ88Xs?4-muB/Z%srs\ѡFcɑoxw; kV;^I:[nCD6{zSxIP@2~uILgWwՃFFL w[7 ؼ<'euO?ӂF³BP5sbs5o (t/ּ@.gј+OÏ綋SS4Ǔm"g\yVE-ԡ>>H?vWUu&\GWC5i3`s\lG$*`g(C{""H`uUy T2kYy)=M6)b$ي \ VBAkTTdcwvK\-Khn ;^HYV&o5ɡr0$S\v"2n 8R ϫG:a.Nwkn|YĈ1WNELҵ~uQ^Y j̍g 9r4a=k %thWFjh{~d0QӾ~vKqKV$u@DN!K!LurT?*2{(%YB[V _tVLj%YAx[8t1R߅w&mOy(aɩY(`!iU)(n-Qpι cфW6\z%U_!u2ײ0Bn/J՛g]{'K&K^hk"}Xą+ᛳ!> 27.[b+WתjrяAv.KJRU)3ՏْWV\n17ZRA DZxo|l#0Eom+QdhCog~.&Q\o[?.Z~3N-n,D/'e5eE5d ( 8T~fD-yG R%~'REյ ׋+ S!2\DoճtwEhoaʿ`%ZjMǕx=l9BϽ-IDbx䩂OWSPGÉ5mQ/lLTD r4zt yaB͕/.?Y ʨ"&KuΦg$B?@?" M/4:1)?j|;7PO.8=-"q *J k60qc:7eiڗNtvǙ$',W+A-u= Te>`D?uFL\JPJ qHE\v"%)pcH}ʉcu @q!teTYK)3n66>߅|BX`֥[mi;r[&%Uvo8qqɢ)x,{p3*fs sdZ3 Do82n/˙\=ym^=W@MZ0X- 3 ǰ!5{sA*?d IЧlv'D6f"P9(Q2ZRHq=y /Pj w[UX`;_`іɺS"%vn!a7A0&S.EݮK/)Ӵ~!Y˒}r͹Q҆VNʋH\؆qnb&gԉɝPDn. ~؛Оp dK %c}K*\Hr"aZ_Iobz~6 <4')Sa(s8Sԋ nN6j0YӦBJ-#HD I`FGB<As֥ YW&4'<@XJ lYjk? ,_vDpr(%D GNyd3 ώ.]^cskeh+V\+be:IL`Ab`Wp6h2C? (6H=ݚVɋp~䂧^l +  ae$. A N29ڲ",K=ړȽ(mv[88rM9".й$II9 ~<+'s۶ݍwfHgk@@߄zw-ĥ!!O9\v͗C$d0AY~v=٦`< ^ S@a@av Sn/^28܀hJaJ~Ymj٤~WrKHJx(v! jJF{LV6D<ߒM(ϥ93.W 'N=30CDuXfQ9AxͬL ltj/<]wt~`sqJX2 {7SyUq<٭2`nuK0m2wڔr _35-ad'zD/)[e1ᚙ(E>^O}͖N7+p旄rsd?^VҐE?NGĆbD6NT#1nHSfΣeU7i]` %ISR2u:C#%oo4 /JO^2Hjц@ſpҋVMp 2(`"zbHsW]k&;J.v^1% *ȊS`S >isŢ7FcB# ClLBqr8HAa XDl-&X岽+싞-+ g瘥&#O\5iϷ+@w N3f5?fLƃs2ՁB`4v>7N5mp;q&0܄Gܲw`%#'\]I)C+0NmZ(=Ȋܝ}ZvHf eJv O!+2ں2 Q{kanٛ+I'w[I@7觭ũs,&dŤٰVf`4-1m-* ԭҌډ^2~ wJZYmstn9S7{:.nY{>Vʚu/9hBቩ><2 Жkmgj+ܘc'w;ttrDRH!G{t5Bf/L!nͺEM ie$I$NoY(ɟ tC3CP+bդ4P͎ɚ'Iu҆ݻ ‹lGnu̪HBt= т)99K *)=~!'!k$LNI)ȼ(4`"oFqaɉN ' frٻ\yJXls[ ߰A}ە{F1${Tlk@ˤJgp N!+ Rr?Wo3=pdRHsCVbY@zf@g&{0DlTЂm/ɾua ç)CBIXL m}A,7hψ=`{Z/R&0=5 3okȪ7%8.5eACʈ'$]O&u3+l=D]ʈjlZBK3 "EZι mڑ`"GzC"Z#]l@8;YPV'sPbIF5KQI"G % /g{xMV 2x7irec>4Z!M]F6Z_ hQMѦ {iڱU21#A< ^+}lכztz*Sʸ1E85םҽ8)T?,w JO!.o݁f^d!@C oXraa:л 255cB(ˌ 1D`Ntq _Q:.cmϗ]XxMTlP4נҊ[01^VHB*oKC8KoWΑ@2!_L"XwԨ#Xpj9 |LfBrX+eWŢ5-cAV ]E0},a &FzS8!$-g, -OabBxY$@ Lfs{FϚ`̴ ^ ˡTB }ըRgKc<]{ҏ^ j bYpO%! Sٳ䅖|Po~_'Ktd*[`DJi|1CrJ$WVW@)X('?A*#5y1f^NL<1f*$0:tOM%KY#!.l=.9kP/h.|1b (ZяNM-uW.+!QPb򹜙}; e`śi]bϼ6Q?ݤyh[-gfu2oEщLNa&fUh I b|ϚWQlIM))>XE7i^zm) LrQCd3bЙ+Yl 22c'I3Hىa!jlXZx'E q Rӣy,>7,]cLw*WHs Z 1Bؒ#I Rû3Yi1C^s INmZ2lp5w߮RL@_<|ux,>=b9aXIw  ncY +:5؈n;&C 27Xv"806l'Eh,n&_= d4'H`z lsq }Mҩ Dκ/.9͝χ7 i )øngnY_7(c^G^$I1%jl5*Pa.om75]!Wf7A$ޞjȅ}Qv\1Jv-v%.;GZx[rkl%fpĮ:;׮SGK("WP C @Aeiu>mb~2].hƔH2s_&ޢ`%#i&ېz|;.㽔'Ƣuqrǂ:N ns{TO0(d8]w{F/0xvKHVݹ9:O:=w4{Xڳbao}`?ȓBU)|q%_4%C7IZ.!#V/x,NNY'F?H )ݹOjù݌;5/xc[/lbE̵T@Ym̈́+Fg| |C1s]{ wIKy<CĪ \-s](8 "sEӄVpڂQ˛L.1k0d $t{M%s(+9x߀ q uݾ_S_~c̈́@ib y{ާ63:Tq Y0fV9?pzEe/qC0be#cfsjw@Ő[On=[Tܶz]iq= RH?" HjQ+߯^{WPȄc?b&tz߭ AVABM!Wp3jX(P4 Z!ˤu"3Q5+4׏֐qrj'y] CbBLQÎ. YPڻ> S6te 5>drHahN/98 0ιo\Ԟ}u,K@pMOp^ ~Aࢪx$tmrܠ[Ele{˺zW^*7QLpͱlC-y9-!~Z"wʹC Ym[zwIcDŵ$iƄނR7J9w٥s3ъJmڄ~Gj AO@qceOȍ leo,&$]  MKFE,4[f(hƽhTuՑ`j3Mʏ&yUSGSTV Uk9&_g)-{^햖6f _-%1`2`uh٨̮q1D<6|[/mOʟ 2rrr 'HV%8].aݲ7qL4)dGCJx7[nkOZ*"7 e/2%ȏd6`8մT$jF ?7guԃhnb ;L=kw |q~SFN:H{&ҧĞDVXj_삫+r1OKĤDL55r%r$C R\?B;뺪![!\H&^ 5P$CR]bҁV&/ă۔{b Zߴ^G{CB-Z ;zfֺk}'Dwhc$Ct?uOmaAa+ݱ+yT'M(|W-_R(L֍uaFH"O wa@Whk'iwcc! nd/8?ZC M6 ;k&XR͏0S/ϩ@EIbmabW|4'*s2ͬSj&ܠj6Edys4^V&B!>#"WQ83&2ͅw;C5몺%= aL'j7K^DhYT.D笴0(Oo2>H:Q%x(BKɏko]t'уp:#d$Ɔv$86"DzKFHNָ 꺈话2ɓ%ymԑr&}{|-D'BNG(|q. _~] cCڛ /2Cx\kh%ɎoN!ibň? Y+?bN#8D\u7mت% řx4WSlaQdd.:cB[2<pM "# ",_XiH"+rM 9HP@r [,]~8[N LqònƷ]1N2 sg`= QG=p<߯p.I:{ƽ$ӭ4AL0Bc^rC_^4 CȠC K73dkp#G=r $UCJK&[0i>q7<_"Xf3  ļ(k-s5.24u8'FⅬgr{C7WƿE+M:Kf%!]1ϟG.n;nq%qPюbCf imjJ R3QE:U9s2 |]>4O[.73 rbkXye Dv74vLb GU5㦆kh[Mio^6;.HnI×]N Xш[~5v7p5n9\cYo$!8塕XIZ5*'-&84LWv:D ?m$xd3t!ZR,z=f?&`4V_?Z)6&$ Q A$AYCCsْe 9,PE=egm}^Ֆd5;(teBmaR mZBQ@neH '2hXGĕbrwJ i8jDӛĹ9IZ9u _95קyZd5j tF92:%b6j= 7k 90>&U' /+ʎ ט[,߭E}z .7&y;xi6ݯ1j@'#IoD1 Sm>:hP9ȓBù(~p!~|Eϑ tt@c'fWB˥d*`35SZɓk[FWD)z$xM 4g}k7e$ 2"ć0NݽO!I[ xU) gI-o~0-~& xחx "'Z(\ o@%|~2p@"퍿b͙W3@|19:ogf5{uf!m֖daF|5 tx3|oB6587|koo=BGG'A] ў|lA37XO_ك3p~xe6`й̞Ϳy%>z_rY8|J߂zIByIU./x`ӊ79G68+pR;Dj7Az4^y4ivGydݔ&Z]RBwieGĀ F]EEJ5ioaDsLYLKjwi; [?JQa)rR }ǡ.1bDwl1;:KEVxʑV_u@iJ"ւ2H(lWAFeE GGD DᄷYn,Y6aNx#CNlg tPK .7tT*$ȡbP2> [ԧb.v/QMz1>e۔M*YpUK+*nJ(D8iڭε۹zݹ|sɖZOJOz\[y^jYtsR/~1/n/r=v?iѻi+Cԣ &66f_\7pF˃ݟh3O gnۻ{=Y6m}Ӿ?q FKNwu_H:ԅNJzkHJ 2]y@I#AP>I#7*/y 5J4vuø0q8f-G6՘1c [7 ->zEVl`QP&Q e쪓09-*m2rd- @oGO՟UrXɞôFO~?K{~^껇oV>Ѳh{=jwhd{9 N?lq?X:j#b߅=%|D/\kѡl+޺D` -/Kg0K.v S}5J"b{r}%}TxT7 4;O9gxغպP.9_'_>L,6a)Ѫ͇ȭ< v&fC1aZ4p%5F]E'ߠ*6 <$P|f-'Lv̶< #,}!1= iI' 0TG*%!X;@1^6iC_u6\~ZPH.~o}BãrBBaop\.<[(<_( ӓ\clL-wJ׶[RZvg ; =W>M;i/A՛"9}IÅ}  / zu%=\7eH%jˆU^ĘQMOei.-\>+qg0`*26^!ٶ*O,-UQj)=yJ6b[ˉj!ԅPm`S٨y]tfUڼd㉛6!g A'AW3Y"5pJٮ@ajKJ cѠm R7m8eO(>j( E?N.v窢 U?q;g#X)(Fݟ\Mu<a"JR j-D0N >NQrs8R;I7K͑hi}RԵΨ?pc,7k!-NW`K&j7́ܯdfliغRl mL.<q=y5.P&Hl"JYa09 &N^:^ J-gCQ٤Q@(D-\:'}Z&17M6=QkD~C)'#fZrjW>li0 3mrN;%\&M{i"Tl[UX $6E^u@d Kup2qЕ_`L*0t.)96qS-h_ݟK)zځd(6-l=9ovdSM"hi[i3RM_;(~tݲ"T^BSBrDA}ҌO+&1ٴϚSCuδI!⣽P`//Hի9ݮL{Ed8o9q r!d"Pspg%S$O2lBh/k1x?J?Ƕ$!%꧕\m$/eX JIa pmZvJ(5LM)tDEw@$&8Y~DRD^TtA~͊wSn,B;KcL1孿rε5v.aNItM@ނc}X Üse?Y6m%  [ӣ.Mv׳GY/ 䄤oI1dqc`u;% H2妍bJ(iL+ƃ/MpΕ{`}Ӝ`=6ez8'fLp2m;"C9nfh${a)Iwk3ƞ' ys9ZG:9.(hT9c݉9hl3ՌeZja71i@$+EM1`neHFr}4#f)m#u`Q}_q9iC$iQtbXkX$ϫʀ6Pz]ѵ  ѣps7[q+Y^UJۉ@j١ȓB$>%?hQQa˰aT.!Bkf=Kc`w50o 1+gIWc\8?ѴAYAYlΑ7,MCARl&s{_4qM1N>-n'.ICm3~!ܜ)rWhOeJ!>Q^l7=!o* ۺM81Kë]_&0 sZ7`=AAFRDNgNcr@'H'gYϵ|Eݲbߌ沧uՉ/ȦaZ~E:+f<4ԚB%GO@a{&m44E+ yNHמ<7!BQ ð&;0%;ȓB$Shq=Orx4]]LAרdSuj$\׋nE%Ig HB#b-$5 ca%Iv21}'jԦC 01#k@' sg&zh@2471V% WFe0qC BxYX@ٶzu08QQpl"ĺuOڰw(nmӋ7$ RcHzݦ &㪢HaF_-4u6eD@2RulWZ]c^$&U*|w'q96Ig@P*I B -dFR[n;PV3SrHA\XwN/owmB鞉!,Z ǠaBN#Q\}hUPm:G%y?XgKz#QOH8b2l8(}N1ɯ" bIRE됀%L XBm #`8qksz""Q$Q_9Bf6wob C%Wt-t o˔ZɜIWm(/bιq6 }as.C${Ւ[[P#ig$3cް1MdwCI-l7Lqfѽ1EhAjz/LD$bWћٵn`>2;%tiXdžGoIp۟8:z9L'Yw "/|M Y-|(}yp[o!ǧlZsƜJwgX@:znU. QWABbt$ԭ[QǚПDrY:nƬ~aY?GF+T4Z G ʦ)4E:m&|æ2] -ڔ8s!DLk9-c?S4H/Uv  TO]ͤ;Fv.ls]Irh7_6#L"),ȈM7wNI,2;ɵ#g˿OQ'D _"Uȷ_& aZJ{Rִk!G,nO.yE8#:rX3 (ַ;{+'Q"1^, vW"&=i烄}[.C#(It ]hU/M.[] N@!%.^ -!콖`SqcSM7ȱ³r\)evH9nQ'ۜai8+!6jI?l+rvDyVm$]S)0gB|ݗsimdq5paGD9ODE-`;-NvTr:LR'_ۈd"tgGvUu lP7SA%'K.;PF~o 'YUTA+ d;u+B3S<)D"WsI+<մ:L:CrQA͝aݧKTۣFx`#5a-Z7P|HZs^v;$Ȼd]2̠EZ 1NRL$HǠD˓ l)4$7,4JգJeYf`ӟ s_ ݊{Xd -{ٌ1Df|PNa n~ (>g4n`U'uvk "1~HBLDBAB YނF) VzHBL ᒡ۶}ĝm - rD?WؚmnNշ3zVWD[dp}{[bTTqӟ!F^w0%hSă/~In#S7`%Jw-.~T`| 'Ț)ԇ\ u&<a,8hb"دבֿɁu1خG\Asߜ vMOIm";1&Ski 6 LfyD1=^ h\%@59#tFtzҘ!%Qb_/ l$»X3coUӖ {hl v|B[pZo ft<'8÷BI^ %??Oom`xV (iČAls#{YrXƸ2<]|JkoAOpk?jyuš׶oi+$_R鼙=lIBp7o }/ޛWN/xQ` G_RO~~*IyoH>/N>;;zn iOOA|4 zٍ:U$3D*Djb!J2ndF^w0xbh CQAG/6MD1QE'#=wR{J|yHJF'󕷼+jai6i(1X?J ''ǵKa-L=ȓB$ tat@ݰ#hqI ({a$u\ nz6ܠC;"ڢ>j!aǨo[N- )ta!LB$F I+)>~并,~IdhGjy>`A8Lh[.Dq>0PpڐYם3dn C񖵹mJtx8o&؃Th͇wjd9j&x^1' vg6cӴB!,ԕm0[V_+Ȫ[zYz tР+㿎k|\~SPg2T$_Y)೉_bn $(h2cP!6\03LuO!x:V|2}cJ؄%rD-wrIy+z_5ğSMPɥGPڄ t;Qluot3<,ИISbSruh7f@W&qϺM8aQG@SVR-je(顡& I}x5 +i`h? $M8c.[d̋ :Th|@oZK[]3nZ25 mlԙSϝj9 kRrEX&,|3ڂ-.UK(2 Yȱr7MP X3ML@?h7ݗ8Z(B 1XIEM[Kmk)}0ɗ3zoz -"$ɲ"kƐ!֠[N~Õ1BI!o+$CzeF9]-XlӴV>/k(1%e /Sdڗbi.a7<1 4:`"|l%וk.E ȠRv |aɩ{x%Aޥ}D*C?-\U(4YtmE$ȡv& Vo!+\rDJ[u4mGH$rYtڜ+7i+n9F[V)$FRl9a% w銠j\my+/2p#><ƋXxMHC{35-=Y/HuшD[~A =ᯬ⃌ 7w''P@ߜY76/:l[+']<}++xOXDҐ scNp亹< N5"Y9$$# 2lAE}n .EZ+R&"]CƇ a2_}W xl v* >RE2؇ԕbhж@uo\ܞ̴%(K>pwwQDحoJ7FC\x̭gB-HT'$l|83Ws>CXYQìIT}|{\ζY $-b4E;=ErIB4W;=h:.vkix W&#> ]vɓn2a @#M*)3tƬf½I,@ I!2ԿC,'J{a5 m+ ϸ%sL >zYܓ{2ܠGDhEJI1D8T 1e\k}51kKnX n6+3 j=K>Dq% Ê]vMOmǥM*%.AORxת`@%tfKg1%H VͶȻ3fYHYh[jz&vN[ S@H<\Q F(y/to'܄ĜίoҊA!SyPK{;x)$` Z&S`vז+&iO8&$87 `+Ea Q&t^mWpM mqƕF̀74u׌d\ThנjٺNYrfܛ~GrPJPv~m,ޢܡ曺b4_΢m\^(SbLG`g9vskv-VDj5b?I}h[7`LUϸ"l0c˝Ev_ y߃OI"D:~!*^Z 7zMy =X5/[߅) ].Tw`ڒ7mwƄ8Ir#Q3 I">k-zazh':%5%2'WAxms4yHJ>_8m$uFD;uek"˖: ƻMa^`zzP\`/O[[:Ga@ C4klNX`:Yn N/(K!-tdw5'"$x\> CŏnzOuIwFtB@ӊdh'qk+7[ߝHNߟS)IW2 ܞI?!rjfὃmN )I5l>ѓ66 % $u-L:ݼuIԂ|G.!s!R9=]gXՋ5ƅTHnɈxF kєuPKfWx1g8~^al lMX/) 鈛jO$`ۄ垈AdRZhj[ x6;NxAՉ!"H~ȝP%tCi7hz쾆tKn9  $?VX$\pz9盪 '?o<-n٫& Ki>^`<*N2SQ2;?!§/ fHCޕ;-ph7P€`pkVWA؆ խ(12*NO"\7|Ϙk9yN'1)5i:ɸ 7dQ%Mm@A-T4ucneI4tiaT|y.hSv hO7ԃפu;>^4h^mAxug#R?=6tnQCXih4$10]6B'}tEF ^rKo=^]WL.* RuMBE1&|Bn2sٹZd݁llPnI7<;9rMP>-+r*L-90t R^h@P~U"w8z"!!b&r P2_J~?I^fj)17 -O`3> 7.Fch[̡. fƼJPP[75dݟ$T?p6pjP[b`" %jo گuN1Q9 6Q>XU)FiPO͉j&Sj.{e[-S9UHrhyU\g7LY8B%}R4Omd5\(.%oH*sw_OI3g;HĭF-4.w iaW eg):&G/LA)İi6^ְΓ3&NP]^_4P9L4"u)FfEdd{g)[Dbrg!]598owO{GiN/l |{#LI[ނFjMm^?]q?2AmO '9t>9 U~3*Iȹݶ7u!#BIav7 SM0Hd=|x֠bE>Z3$B LJWf3(hЧXla&M;o<)?BK A]$]*̰&Z^7'Me+wc3zXr'#ǜ՜o7f NǙs9W0 9G;JjEČK}BQ)Dpa@x3]o| Hdg-;oj>7J 'u>3}tMX\\fƜgv\`14"׺ c0.u $Ot6KCAG R-A eB8^(V}dDVvSM-r# @'3s.]%ɻF??K\A4lHu^Gm(T(q'$yx3sfs\o \…fVל u(? Ǧb`nIBQXz7qCt|=ƐH rbˮ9 &]8 . !sɨ<6O:TlkϵKcBo6{w^QJ@pуMZDGɏOd&H_M 9Y;L*=O&~ұ}Z2BH*Z(,$~CErFQv5č&9Ҹ k_1<>ߟ}balVAZ@ ER* 8#m`fr#DnR($$PBBx{XhL(|`f&?#~30P(ѯuIq:/ׅhPt+oךh@#FLj YstTIvFb.b_ 9*Z4LmANKbBE9J>!C0^Nc8y7)3 c Dis;J\2:p*5\ #HR{%gG/oa|i&dz_GohH˸N}8."3/ю/GJ?]UlL8i΂D|9y\Ŏ7.!i~9y`B!/bD :M mȵT7E5uhA(c!^rY& Mz $o ) =HJQ&j-,2|ĸFq)j{us~tw׉dLԨ&tV*['#ʜĠa<LfK,gSrXȦx% xCJ ljPBɦQ!1@0s=WH"R]8(눐6 #Ip'vh 2SV*/Y)BMA]x^zeVfc{dp؛4=$Yn(F{b K Yw O$ xX}5̢"Iy?1Du)rVֺ &}!Ȓ1h6a ;VvZF%͞bDQ.93&K0LY]wpr!RW|K[Ɨ+J㷖0*XJn\r)H Kr@;3Cۋ@n=hu4y0Õ$ُj?ZЃ3SՑm3HʺB==ۜkv@ҍǺ9 Aias)H>^e6#veBvSA_}3]Oh447C0폿%?cn z:$' =;z}{~Hζ}ߎVЎCv오xwD:]Oχvc;%f0rİxqT`v [KraN:&gЄ1Ġnv J# B@d#.+ƈ$#U:>гb>&wso5Iї;Is|9!; OFj!C ~ 06{_72/JK5@PM&SD@]4[vY3g{6"j]/kO/װ^GgnEC}z@u Ezv//f_'?2gCï/7ߛG3xo<4|4 }:j?$mxyv; <;GU\~4oN%;AH%X9xU+kA٪$ږڎFrzsRsD?$%:D"j8aA.)- c9u\4U#p.wDVH-F n8ѺaHˈBs3~ئ BHFGG"?yaJqb`ɺBȡά8Vm& aQ竓OO7ȠƵ䮿fHeH=Oά CtiQ6:$1L;D\3C0Q5ZhzD%B|As5_dtc(㋀qd! fT`@XYaӊIMbbI %9h % Ef3i$g[wW8nܪXe;)9jks?)/2M4Z+T8QSd s*f%iȞ(Bj|xS좟l~5xn0PuSp^/ t+,BhzEb=yQhzWKaͥYBSA/;GЌYM!$7ۜm rJҗm>6]6Jt< \ _X!R [KP&<2KK/!s!RFg ,F{RMH9L4(ݕs^t熫hS{JG5~18aK>봩)!m3OOj9w|+I-*||Rp^ vxFH4 cG8-HL^S3u"DA98qʽMULS5YF6n D)f^Ghyϰ\ C$KWGic"ց@pFR`#M=t@QMZq9glçabt"JCXQ#ѽd ~:?XiX6U"[F~^ovFHxׇnmEg&gI [)s$NjK}LZkO%D~.D }F֪%ٛMD3ީX3=IKyx'z#Dй'&!$(? { 2?e#%9Cd!ڵ%O6Z@6$@ #> C\Љ? /'gp/@yǂnzϰAY+!3Ą̐1/e\ \]b%ҧ+~م';iٟA[D',Bj u\ňːS rͦ]٠t ث \2CKGE♥LPև۵_-%~D gTdkÎ "Qr*s^̃w3>r"!~EkXS#~K2|<1a30$n L89l Ʉ\eWLqTte֘ņUb]˩ Xk pOχ.BAcHdlG7=^O,=,u ~9I_TDVq\ϳDi{UoFhz/Ojzi~vMAlv: Q+ݴ}|'>-Lôkn|)n?e~;/iڇ*Ѧ{nt0 i <D&9\BBdIe&l`^cLLPm,}׶lcn8&nP*C2Hog,VIb9:'M~Uٚ&ԹM" VT-Ea])'?L|kُf8!#HUVHI~"J> MnMí֠d׌1"&U3i0e& |?0'Z_G]8h|_Jo:{u&㲩B-}~ޘng@S^nNvX'cL+zMiK?Mr8#pě)mվM7jZÒ6Y޴UVGڈq ZAwV.ZYӗ[vji5 ?}q<N{{0{wr=).bhc;qd5h8ףCTuAtbi$x&/" a"Xruo_L4oVL7 ȗ>1h\ϬΨ}u kJ\7O}pjc!~"(8$뻓9lq$:1 IҜơ=*Z(>r,-y,rigpsa,M,PufC/4]Y"V QL=5`:ĜcT3qoDt43Aɦc"~̙rrAP5I=Bn !{"HG( %+gcdlZ$QRĀIIZ tɴ$9[t_-"(&PEfnzdnР%;ijm=Ч&Ap FWMKH?ҞCxfMԆav̽Apm?Ibk#oH6Ȗ%t:h-;Dui%m;|^7JwFg:X˪)9If4;<+h"Q66<_W6bsR[-l?SU7=$Iz*cbHk `l!b]^[huk4xb;Os-aq{*t\wʎ6x v[&3BBv١+ \R)nvv Yd3JaI+Z\ QmF[u[!ib@+ -?<MdnDh`JrBKd Yjע?b!BAݠ* Pm` *>.gw-Лзw˝SpBEԁRe38lvTP6YWTop"S;@X Q9u\BDFws:Y8er/RxM q ^_~oz%?bB% 9fXI8!N|ikAǙ*I4C 8K\h0ؑ+2վniN\ct6\ ygf>,A RQ_auנl4s8PF,FS7K1] :D,oNtn޻heu( }  9R@Fʁ uɕ[?*P -˂mO! {U= 9Z^+&ݝ!Kpv 3p(9m+Q 'ÍF7$u_{ǥ4G@u/;Dˑ@3 l!h/C*>Nɵ|"6 Cm*1  f_&r"nݛq:\~f\XOjce QE6%&(Fxn}xgupai TDzFDof 3rvhH`p@ 6e KHj`-6Iar ޭH T x1Ξ(OҨH-%ӥ]ETAznVлt@ 5_MlLj?%D~Ai% d9b9=D"&:? R٢ǐp,ITXtft dQ Af*"|v0{6RQE栒%0np$C((,R>XCl,N'<'sb? G+y+𻔲Z;q.MPtjg^~.peTMkbzj}B3 ܯ=2 zo95hG`;:ۆ,4^@ߢgi,]솱FRj$}y4 )#&2*~&||e7U3W$9Rշ~EF 8G.$Oܩ7W g[((e-d,*m?4G?E&6OwvP)/5zC63=>[~Έrn9-Dyi& г6,rx+$ )5 #l%>0 #| Tmʂ] rPLǁoA[8 P2[lSS+5xEJhtjAυH9(KItX=#ܿd0%l/$3PԠW<{FaCV7O'kcɆa}|cӍpRMzb gܐTkI p^nNӚc廞kSڬ6r} '& <4ki˱skǶ$8ؗիZW 3-H2f>Oc \7,8J;F<0ikA\+p{8& an0aBvY]C}j#l{]&# LSy1lLzz5F-d z޸m8E+X eE.L$d!1)i65|SF$̵8q}u 4%WB2=(ŕ?x_Ф@Ȟ#LjHhYf^%-]`VBe%D~.D*Z7-OoM~=f7(ɐ,=1"Y@[z^q|{-m=">B}h|g{}迾B˭lٿYcd zG`Wl/vn&hԻoB2HwOoG3RB ´{S۱:l9YzÏ񰎈% vow,@8C+įu^зuy9%H,eF&HuD= yU3I̩hCٱ. ǽJb6\pX݌{KN<͛هRQ MXpyB(@ K2h:;vF8)J ڲCRQy Js\قKuL(Ǖ|VdH"N{gŕdq=Lw[6`0,,%*I $TX 6f7ٽ֞vO/_;FTaWn޼yod,'N,l>~ϕY*RڄVUT-^? FB+F&ġ+M0ׇs^cԆαrfn Wc=a> C< K=dA_2=қ="-$:KEN4'#b|"%ɴ*B ?ajo P%Rqx"ǛORDV[I'JTBF&/֜)lRS)aH4%$5W-&;yhGf<C96a3XKg#qk,` 31%~uod:T<䀐ptvs&@!HDZpzxn&#Dvz8Ǩ]m3%%Ce".I| .;h/ W mx]29,-g"d;DD$vٞþvl,{*mzܨ]!1 -1fjdS*QE8=FK$ͺ%1lv$xTNHH2iΏ"C̼똸b6eJSYvC@ԦCƑ6..Nٴuw ǤpͿeh3g&sᑠэCЀE)L5ƲT}+S% 2j b ^D ~i@ēD{:-vA6ƕ2tB( *!L*F^}W#KvgAnF <=@w6[OkOplw*.K OdF'5JBOIDp?{EO"m!`)Eẕ}y[փn;SUs^0\s"`m 8tk6˹$@Gf[3*Xim$L"Tw# .' ^u@$ˉ~YR߇^T3!3ūE]@|D|кnh2HխEyK#ȟVD2RϚleڗ 71ޯ4\7GլǫH" -ph$Ď1#185O%0ol>8RџMDΒ 3ghFo#_=P=)x:.\oIR-/Eo ZMWoCX1-NzUA.1ݔzrX"yԭ1hg;5xsjcɑ9a"#!Q#3;K̀!ji@a$/;``?ۍ{- U8Ҷl'sJ%`Fm@I8>v L5!\ (όDР▍sYHXn&]~)"=I*)7F U#J2[L],D/G;7doc}4P$n v6^G$XQfŨZva' J3l.hXDvL@f{':Noב͉!ْBAE |M>MoK"DNds`F)LHf>sgb.aP̳@r,1Zd  Ĥ܀5ѠiFz]"S3!AП4Mah3TSf/ɝa@:n{q݌ܤy ZPpNH !< s$)TRQQk'Mvʪ8o ?D fK2KDg?E˝&,.kȈLӚF #Laq [Xfە^tffs\kJB@D9Mv6G:q(dXB^㲓E1)8ӖI K'/3tSHoN mm_Ҡ6z؜юAPF̼Yʄ} R  B$Z;&RE2r3w\و]T dG+?M{xtjV)2T3DKlM"`:+,%VK5cOIZ>peނo@/i#2c| 88'pH"![I2*D X5/K3GLl#6*iHL wڗ R`o" <=RR`py ~)Bؚ(C͒iX{#;щzi$'8n Wȇ#m$lU*l%gɸ^nxo"gmv) B(.<=J.8n(l =(3JU<5VmD$> V(@4(q &MI4ugXThJ)`R%XD9ãBDf]KxO-pNIf7Tf Eb8(Q6A}R3Ckjp̷v2`8&PD,;9mwU#3-zjvI=\;eT'rFЉd-y?ϣq}_'M Y";eқfP@pks,y7N׷ [b@)%VNyxa"Ѩ2^AٍeGaف}u 5Rr)#lYaH&.k!S0W56b2G t ˰1L .4^Qy-Wu)Ch"E Ϝ!p/2.GyDA >LsXɫ.Pσ0 Us:!R=EI_@kY77nt: -40 &0ƃ1 &|iu jxզ׵n-Woo, *:!VѧOͦ7ͣsڞWWt #VVԃן6?S}Ϊ>HG^V0B}Q]Zb$צ6\p(Y ;1(|$ 8,(0 kiꖌ(6nĊT Kу[ a6])U`9jNӐ㖼_`Vh՛5>aDJkQLPoօ$$[v m@6VO-RoF$Ⱥ_^>Q(idfըQsPz2\`#Wf&腂 I48TB^CRMd"o#=(am:<8CF"v1QGG*jdGp{Wĸ۲X*QDxfzWTn. rHw꾺.NN%K ˆH{(DCU95m,4^ux 4<N-VA(Y՘`zȨ`cx>tm5]#N.+y=U-:6~TaK2K舾"MWW}/+_uI+^^TUڙZ)#JG =téCGR Ay#Y7,;7_sD[HҒI :PC)e ZT!7o=T$Ydvcsx+#X!PdF-<ik-q8BD=$ў8bb734}˂qأkd(QG^='bgK6I۴H&@S>HM9e#%Ht OAmF ºSqr}ETB]jP pV 'Bn!sMP5`3 - jd#8\kكVƽsݹN}N^z2B0#6SR,5*lT_9?u:T8u@K^a(Ȳ`6SAaƪD}[IT`Sr-)? #ڙF8mh3^:X3SB矱*[W(qkkk:V 1"3q~7?H,n/Ypf/OVl -Zj_,zOB@?Z0* . uKdFқOr'J#+g~hO7vo3w~m:nN}r?3O.K_>g]t\::TcͩnN}y?~8nL jS>8Mu!בoOK\=յíoO긶oS_GtN׹:Kj;i|~%ӵt6* {[}^PKTWLu#GjyNѦfJ[[5+Z1i֔4x`]ݷrVb\?Uԉ.!+t.UPWl:EGtzP?:荧O>nuVקL7\_@~mZ;MO'vRL-5b Ԝg'Nv>$zM!`#B)̒i$˚/FCI͠/P!jTOp%&/C'K c+PE$Ss1B+ngtLWwlkeAQnq-K0#5&,q'nR Ba|C?TEW=GC@K!N܈jL2Ri`m"W$y{ou;=N7Zr+kvr)y,{ê.;IM{kFbG|7Is)Ĵ';^T 8Mƽ>Zsx4)49Bd3#EOV"HY-HrQ xቢ*BV@1E\b03"(5a}*D` s ';] g6$*Q`L s4<ZYFpS L}myB='7уE9P^ $a(>fd"7Y74ZŰ;o)Pֺ۫Ԛ=s3>Ѹg_r|:-d|s/ ’aZҒ Q.U)䆶v{&̐Jk7EA˓H EX9_>/j IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Data/stackWindow/spell-effects.png000066400000000000000000000503061342332007200254260ustar00rootroot00000000000000PNG  IHDR*P pHYs X =tIMEK bKGDCPSIDATxLWYtf h#KM;LIf3gynԹuz Kivz`o_ -iP ?n3t uf@kup"SeNd:rgaRLd bﷆyg.ZMf~@-N>8c>-˝07oϝdfv|e>| R)rnB!=?"s!n ‹90@ű)>_/)sqA yK io6B _/?@r qÙPT5m⇙eB$ut:DƟ[%ʇU :(0'?TJussiΝ ntpRE) ^I4|6⃇ZNv۸9BiQ@!dX43u76B- \ `6PAH`zm OxM"(bn5Nq3WMyWdd % aDi' `7.2ba5HaJ9Q8`P/<7N2 LW2-f4]#`0 jO2j(Jo Tǫm"%x :.lO13gǖf*TE bOg xƍ\!$&$0jiY7f׽e#|j}-~HqvWOKkoM|dws-pQK6gF_9IV;[W/y5I|2\Dt*IdB')p$)cG$Nk'8~s;#;s+;D.BaoJ+"\?7g>y+x I|RYi <*^~~eѨF ڏwW>$h|K!4;D䫡|jFGQ!ߍB6`@Cg;b}}PڍװP˹^Ԇ5cvMQiM] {NPQk+y6D OǪ n6B">l~k>[)-).N+}iy}RplKZ@. *|23 *ɠjhg۸+&N'ʂn[i!tQ>{vn H`*X9PD!պ4R4ZP6L"UiUCxZQQCƉelYuPܴ"(ŴS5(o*_B.tϸc*8y] %ܘ)FurUD2Hܴ8hgw}X⁑"b-'y,-EPWIy[K n\X #BզF۪ $n\lxFGs5P?:۝~ՈHk<$_ *Dљ6.f0QX{(r6FDi5rseU+c GAZ@OWۊ}͔ҍ?K81Ry`g-f>22Z+ԳZT)'-UH: ':M.}M:>*m*ږ ݑgk͑="E#~Ӳ 8{MĭTb"B+Aܸ5z@WUtu=39Y1Rە{j$zmx<4tq*Cǣ델,\'NDتFUD\]*'ƭc{D8VZl(kYx~TWP6{Jbr!Z(@zm`4C˚HF*kӷmM@WƹBH!υه+wm՗BOLг;Ƿfޞ'~7 cyrsav`Dy|?M>;P8eMn[D(9Kp6q )`'o`6 qI;{f;d6\rTS*{˓c/-=yN> }yy-"v8,79{gL80:HE$(PI! .-ْX+*[.9vI%'~fp*U[[ݙ>~P4-3i#?[9 -JtVv_^[Tls߹DB8XHaJQ,h}3= 69Zo3(R!' q"gLZSk9#d\ N _byU;rrWx9d[-̸q 0^-p?]!]l\pr W[W tv2_U 3aOS4֫gK1&${yA2Σ7j֎4 CuH`w'C@uq:CW8 6҂hCk; {^Χ)͵5<'>|oNMo0a[pXrńxQU0xdsgSzYa3d<ٸ/kof]BhΊ=_:"S>|PA<ø=w ѻa+޺qOwFkEmO0"s{-t |H5~SENPA/&T8|fxOVE3hTN@EJIׇċQ'jAW%@ b}e0l֫Rz?>aO6As* RSkG,PR55;WQuYY^)0q'e%1ΆE_Bc9E;IݥhzqLF( []x(2A;o2>4*Z*(s4ӽmD!AYpL`n5p ķ<»8dxL4"+DqYE! _3Ҝ!M!QPݨ̼"rjѕYӸ59[!URJIV"߅0 ο<4w2c!3ke .&jp$&ɖ1|AIO&ȅj`q#t@d<FDE9MЁT~%^5z Bu/oDJ׭&u^5 Їޜ~~LZ>oPMqVNIoOy~CT@G= s7J>"!L1)IY˛$DC2h)]II`"?C/9)WB |D^_ Ȓ [Ne]Uȋ|4\eJ67?#Fw褱;1^>jsFh ]Z"4!qna)9 %>MhKT!*封x(!DED6< TD$ꚍb os4S jVĉTi9A ]Uo@ץYAn$QOɂlbv+:oZ`If oJKvGLDIHB!T^ګɔ`v7<29`Dt6TZ4b, P`\XBw_W ~Sa_It(_;džbJZ?!? |DxCw:ϣܮ,2fTf֤2Q8nBEI8H ]&t2⸱H̥wO{XX/-UzNCՉ]2L[QEiP֩h͊}tȷϣ횓[hB|gּh~1eV"+>l@(H6}ACdxhb4I ZQ"kE$}v D(mQ)(d+9U3VnNEȏ EE,xZ$5R3e80 nE_VбB*;%P~c:2EAQ{s[&qPpwJ3 >[${ip ER}ӵ;ES *M+*/e"ӉjQiRfx 2M60_(. D6wQ#ʎs =+Q4#Tb;EjC?d@?Y8D-|ӝ "G S >OkEnUA$gMh٬\YI[Z Y y vZ[ ~M;|ސq{a"),jZy΋wnʶh"8ǭ44̈́^t(cwH!ݸQ慠>Q 63d_"p~^"Q*^sSwWBɻ-݋*Uke]x4 rGżKF’`f]`pmFΨ5IKؒݠdjqJoA<_D||wL 0"|6Zk6UsW ?i-5Aġ^>,pֆ !O;QšRb`5)Lx幚GA|w$,NUm LZ[B2Ԉ$o#77z2`y ]q-_[(!>k=SfRaL"l g@F\'rd5713/6+eŲȲ_XSy#$}$,f#`!c9" Mv`գ%>޾n3qi0y2 )GJt6y] 3Vj+#LY,]alvmoPcn2px[n.U$bvsP@AJ6k'dy!줒WT3T%'7 o ڝBPD'4̄czn9Jܪ.mFG-uRo_ePH(2\*W*]V]ƪ.lT9׾fԉֳg rrP~@\o+;uIQ{i}d+ֺKv4DQcѬv`%\:(a].SaN(IdXdR!cjKhwgOwsb z)*X(y0`{[UVބ8 TVa-6Q}x3kϏ@K|P DXN "0%LX3Ae+ IH$qCGrȬ;YTSxHTwW-w+qPd9a2OۂWEz0=t?-L_ ʜLMpT@VPE"2Fe͢š p_},;k҉zU z2X%)6E,yX ]Q=\4[kFw]w 1I- TDL^9IQU#\m 0B6V7ۂ>H Ia؉wc U <ںaYC:ͦciO&y!ȋ&; ap[`ܜufK^0ɧhj\pA1m Xa]_=U^65[kwcQ6 {Nj 9y!{=[=kنFL֍C њF;H i92kRohJ2",rkn[P^SWópe DZ/hl.&Ub71LU>N3hOo~Ԇj1ZÍFnE,nNXX4:nE2ZFENiQ2̘tX٧Gkœ>^W,8㚇v7B|=U(<HBnB@${5%ؖ7BW.#V#T,)E+2 2U$'/z7UXrº`ݧ!V馍Zdg,Dr(CdhZ֫(Nϖb1 uZ{!-NiglڇeUQU`RWjU(A $\qMtAlr].DZ$8ډqejl-^Y43'ϻiPW/G ztI ߹ֲ樴'Z|MRbyR^"@|u$| 6 39UHN5Szx:Z(#Pk[Ba}6ZZ1%(ؒyG5F~kkBV]gU d$y}}Z=@f~xZk "ZN~V*.33sWX鹲&6IG}Mf|I z: sȠN/вHu/Ӳ[ c[;qk⽝(>8߶nti0tѪbCT "fTzNGt_@{7}܋<mHP[|YCCfrnLkV)/4T+A7ZQ̎rGҞjp5 u&odp0.Ge]?_aQGIMtx3tcҏQUѴ@ a32yc&Msm_!HX+"۵O#0)-#oTB/ 4?^ND >@iNDQ!]p\uwF=Z<@\ P+1kiuGlES{.w2:Q Yh.;Ypo_IJb2JGX52;NeI[Z{va&/0Īzcyqn׹|::*-on@;njofT o%7? UE2^zf8YN4XAd-KJ*2-ͩ *@ KӾshoc"GF;f*֢|gP\y/B:W,kqfu*#^{3Nš'7YޮD( h=0D iDBIikL v2"1 yPt={d^(-,ho%ǝP'SnfnD>j:q~&IBNc\xX鿺o0RFi3v;0 !]{T9g}v^Q[^aW PmqwYJY?H Ap^L ̓ѣD2ĒfJᰥԹg2Omgi6*KX`̙m~mRK }r1x)Z' Fas: Y|{fef1zcȼv/=jpp,}}U@J*}tˌ̓2Ɋyx=ǂםBb׳9`^xfjo#<v _b i'М=&\R3o^d1@$sA^0sP( Q]]w!b2Fң~SݔF$ :U`!)tUz ̃CCljܻۨcӼŧChpMסhPzgz?nl<),~kf=ho⑞433PnA4_hx7ֶljyK:{Zvp-f[iRXO]}"a% < z[#-U@]-O8ߪI9 V;<+4۠|V@5LSn'JK/0泷9qc?nh9IZgs#7@_O)0H@x8,ȣS![MFr)L_oXME8Cj:J8e:!j(FYZteRY?"P.?Yqq>4v]L1.kv_z1` %4\$PTmJU?c@,e9LkbPF1{%dp 9hY.ŧa(xm4aڣMscQ ~Nlp$3E-㶕 }=`PrŽq!iI$Qӊ;ok+5N.VhwnI ܜE!KͽxxH_'"D[`* 4-VX̄?*UYwJ|:х"VAiVqN%5w03zZȾ Mѕv%a“<:e>H^d1<+ڕxdv4z#9HA_}'#2Fzhxx Iz+W[ XlER$>)p#[J}8 Rv!mt>nԭJɄ 1"*rT[>m,l*١Kj;vr{dnom!fN1_`@d'ZHSF  R4KPob.]nOy$3.'B}vsvcJSXi;Gءp{ʽġf yI/XHѩ.>XmaZ/@DL|™&R$1gL?/~!Këe/@ZȍHTz3|YJB74EЅ@Nσn֜J=gl"՜dU-2uu?bmkE!b7]|['3]L~0v-4 vr)[҈5}F4юwm}j,4SF@rX,/>('JRPWfS,#JR*V5c`벣;V+e5T|,>y.qK))<*Xsa0(XI ںsy]OܺsԷ&Jɗ"ɣ\BǪϰLuҞ)Ɲ3y Y HifҦ| .HM5I)X<ӲcmEFe&kqt$PT^x#4!_]]J@OӧF2DG8JK5)x6++-w"qQ.3\PK!Fy%N_sؓV7jT}E{d;M""h%[.@;&^f*Z(vvp{bHFDg**ɡ+-F3#3P'笊xe[ vd,aݫ7'+WΊQUϛ<ٷ#G+3驸:K qmx.F(2n=;ؖc O/Kk#?f[x?ѐJ %8%{:^QM@PMLY_aFy<tżtl'1$Lki>bAL8UD1 < >J<:ccrQ}:rgboو2PcݞAr:ǜƆE/_IyQ@W7Huw8\7 nƨAQ.oIIu@aGrYw"D>}Pje8FlLa ig։ך4ɑȤw.|nT)Y^Noyo3*en.s]uwkmnu9߮bB3qX jUUiL\Ћrm.%oP~=mP ݄uba.5/C!P~pb^ݹu [Xs.G2 CF~Y|ڡ]^,GuN0mm!#k҆=`猧L[1S4k{gh ((yxLOà?&D|]=EP.~*$ߌ[N7͗FԦ ąq 01f }fG&` YIG78H Umm@ t\ ɸ'iDAQH6SA *צյ:Gk,LcSh+J|4ҕXY& S*>%䈒/}x燛q2 Ɉb0OuyFeo>pp#ur?&1'=ؼ-{-7fE$Nd}rI">/@\zu:4vw"y8\܈-2+ VkXBoJ^}# ^Ę5M"{S6ϊdY.濤W7C S,4Jg:f7"-k ^񅱖2mjظigXW5pY(}Jo.'b]oxKRf^$oYJ1e0q\8HjpҧCfm⣳0o@8sqH_=B䇻3XX tEC^0FjqJ(vݢ+q3u@q]sk<$oa/nh͓,dDd{vrE:sp-uVّ*3 ~ܢxnW:F{ɇ[Tg}2g)6}aXipwN?κL#9k}C9 &D}$ה ZQoDJS_3x'p? COb3>|eă!['P#?R"?˶/ #e1.HUmxGI>"Rxʮe%QObQU:=J,oJtXvbDЌs肠Aڨ3 @YTUfT/mmҥ3mfe'&YOhPCQlcnc#䛘O&ecċ CdmRH!)z0aX%!wSSMo掼HD_IsRykKHL 7IȞRNK S#GdBdE/H?!i 2yt;Ya}VOv%{cƻ ُ: kxg .s|Ʉ?w!DZ mᅥI|i ɣx-QI㣈> e찈n̑&+vfD5Z{Eݭ*[3s)dk 8;#L\v iU)!adnqZxba6B,#GY$6-qbG +*.pL`nӋtd}Ew #-b.,CӻNN2ֆsmB>\?+_`xW;r*4қ:=WnLIƲ& %7Xsқ.?LJ?4otnmRC2SmPE&^eڗTd[sypl ]H-F #LK N쪐sWo*5F3=ݧٗu!n,Ghi4% ]z3\2\TbOٔ[! @؀՚mWk coݠ^ Kyp0ND|$ z!߲?xbVG} V0̞yy2; .%[|Bpd) \*YիCach2\0.<7 |,ͧ[iJNzY`6>p9eUgEŽ1k4z(G2 _cc1Wo%E`"MvnK3k2R]yP|{ P>QlcA7ڷL|YsumHIGH1e1$b ENFcM?r6sZ[̽bArb06:=I2{֯ yWe&JzD8\'z  @ XOx!>0~`'^2].i-jXغ%%1VI" ^$Rd<~"s=kmjX*lE9 14`.f 7Oecor&;PxcP0 Q V O%݇B`s#DxVcuKy?Sȕ_fT< +wXBΩ2s4H>uw8ßjDG ]?֡ԢhxfUmSn KRW~DRWCz`æ}r܍&JXS8V:-bZo3Bds)YdHd7Y~%d"W0mf?iƒY+Kf:zD`k/`4͚c)s>L+`H9܋ऱPnlğ SzoU*J(\wA&gdKanڌijgd́ :0Ph{Mž-\m$hY2{g&[Cp*[ ϓr/krjMnN!}#wMuki y&ĸ9r2 "P$OYG%4*6I[:\-Aa$v3 _j/ cW&:e.#\!4}ao޸uxqkb ; Hp2m7lTW.nfahyq(^ԕĜ9jr# *1^$!2_ ۽9tRєvBdsn{5;@YzSq`ݿ}TcqȥAL;ҝ&+F/uuvp{h\H\GlEI@?85;TlbȽoH;Ǝ?C}!oG6zv2_܅ G[u zoɤ4iCY -ۙAд! WJZO?;06.1NƸUI} EԷ$o_U0@-?:]'`YBa}hZ(JC&f4ʌM* dyp߃mL˿Tќ"mVADpro;j7 W,&ѯ0I:zpEsT뷛L믛1OgI]n-HB3fG+#l}z/8 Ƈ0 {WEj\ /XmwmS@駄Yv6eeۀ8yZ@d4$RDD^HZXvV_ p YI DS @Up20$~)ɢ嬁4g!e|G57f .BVct毜&i0wS!DKWtݯ g;tP߅+$* |+n{HXÝqz&HEz}7$AJSF@[ iN͢_ p T ]zJl>o҅ xlEa>AyuG]S+(G\зzQAM5 xsȉ?MbR pdvD*!tYJx`+s:5ސLfnik*Dkg٘yJh,F8b%ҼQ 7/>TA7$N>Eb^sPƱ?y]gWu2Bfu,t„ۃW9Zk%yfP!aG؛L  Ch~ztTUv>0ت`H2R nI^I_. gCoʦL \Agbbr#N%}WFҽbֈ+9.53Zmof;mVL)E & {nTr(uߌ9Q$f 6/86+ LlE0[4~(*)|Y5"_dG';U٣=ISi;c=sЕ 胵]_RϨ81os~r?y=>݋ ?5x6)_t٤I=$F޾5^Ltzg:$a5g~~}՗B2 RR;}z=xGwn[M%AjK] JԀ@x@0$<3̼_d. kNއc AjG迚!9͐!G|mw\MJ! IXĜe\C%e,3`2Papp#=͝>dY&I-ۉ137MӹÙMȥߪ)Q5sy})DlQe/O29!/uB=7Ce kRލ*t#ƍXfW3J&89sApR)FX^+Gd7٥^a5J (w.ATE5,W|Wc'}"*0JAwÕݛ_JEU4=pI~GVcmBңj+$А5F.œvqޠ@Mn>;ɳOr 5_7#[^|Yӽg ox‹Nuïl:&x|{Fް(6VuMz'Q LVD z\wS|||fPmCCnL͎SwϛdzNެo%W+bXQ.`; sMN#QG\q I#YAT+9:pa{>Xu$2w,JD%o afd,K\U%ݵ>Ƈf"ׁ d0yl͡CL :RN: *A8̈́xKzy[ПΈqr*]KB!Y t67:f.Ia9Rz4}xaCߎ{@_8 6^LThV~I!֩5NSb Qaޛ#Ko' &stn,b+SOXdY_mKmgM.9Lyl3bI1pJ \k܋w܋oGƽELyi` +%R3U7lF"hШE%GiԱ6gjg f??rORY8,7jdԪKgɧ {b& Qqy:zP ծ1k|\1,|0bXrǢ0XI%' 3n0,‡V ʇ,Oa w8;22{ad5UjL\\NVtf;N262@ۇum?˝y[niO3R5|a>cᡐC15M/G £&toՖzrebr[vTRs@T;Jh-xgmRgK/u}xBMI_B.Pn V E9nNiHU'DpFzv^Q!ÅV'1/Py镨E L;eLa*ɬyF$R9 U(j MFkt,:.FW8c1L&ӎ9ƌaX: rbl1 {{{;}#tp8_\8"Ey.,X%%Gщ1-9ҀKl.oo/O$&'=JvMޞxǥ{=Vs\x ‰N柜>ucKz=s/ol|ϝ?y ^d]ps~:;/;]7|WpQoH!ɻVsnYs}ҽ~4] =>=:`;cܱ'?e~!ańD#G&}'/?^xI֓?+\wx20;5\ӯ_etWf^Qs-mw3+?~O~T/ cHRMz&u0`:pQ<bKGD pHYs  tIME  3AW IDATx}͎%ɒgqɬ33, bic$6W, 6#!`65}29nf,#2*nCuMjuwfd;0dȐ!C 2dȐ!C 2dȐ!_1^G6?+I4tuӒlll6d6d622   `C`C`!!ؐؐlll6d6Ǔcx2dȐ!?<4;_5}?1B_o~=   `C`C`!!ؐؐlll6d6d6222   `C`C`!!ؐؐlll6d6d622    `C`C`!!ؐؐlll6d6d622   `C`C`C`!!ؐؐlll6d6d622   `C`C`!!!ؐؐlll6d6d622  zư|Ub/kc4dȐ!C ]_hj@D &&jGLD0 KϾ~Ga",MQj^[_>%&{,@̕q(B"f`f C~JxX>k|}}$`fw5c0Ba45"0(*sL h&@ewa}piExOxAݿ֗O @@!aCe_ҫ87FX;D 2$*sYD11)bC!y|z0EgCowD-՘BY *ĀB2fئ v FL0툁ԗOFp5d hbjk!R,f_Vw ygL@zR:?9M F7^S_> 3Awp١ؼ 6ZSϞx%/Qz~TU l'ZD >{KD{M}l =\|&*{"U!՟m4fdrLL(vZ]> hqkgz)^t\[a|DкZN xǍj}Xg*bE 55O03jEp;sW#Wy|.ח/vA.!pxT.?FW#VQn`33]ع0Npi fh.7aPbŕ^w_>t" XzF`QU,ᝥFT 0UpȻZx?PE}v1>{ӑVp-fF&8{%~g(Jxwlaю]iagu<ؗϪDU/3]/w0 M\R gg3~ W֢PTKfz07rHOŹ._[|Rw,dHD bݠ[M}p/B0/^g_>Xz'/ٶ95kMA\Gf@-b ãed-lHETv3mibw#}M}lzg<0LTF 9VM4b>aqLk30p5R[QpAkgmX& Spk 6'GJ."h4*|ܽ"o0Be.ۑIi% yM}$`̄uղs-b,2&1!djx]=w\qUWݗgup "\#D _fҟT41OoMp3&pnNpi˪xw80\Ţ@NU٪2:T*ns}r>W1u,UT*M?wr>W1_Q?GxsSC~bj!UN**!_| ?W15)~bjp|\9UL :)(㫑k'$_Hk!y[k&f wVkJb@cK\ܖu`fݱ́MšjͲS+⇝y KݥŪhq;7T'<ɛVUKS41ld=pJ.j?74C(*L8; TPu쪒-fa٩~ݱUOUqY 90w*@3C8 1f~»qYW@Bq{9 nfynN=׸ZӘPnb$2NcGe5L;} ( *U嫐03p3s|ąh0Yo03N{R Sa@pJ˵W®c΋Xk30SMyYZPIqi|W0ja/ k'74L1F>K3䓑]eKF+jxX0b~"yqȯKlpgf93WOwBΉxXy^&mnAB3މۇ8]ɥr`;Oܤ_6W'M p]c%J_-ꕼ]5M)ڴ ]}%'zQpie43Z :(J}Ӹ|Y )nιV4pצxngF~UTW30UWIV@;4$l1S˺]T=SxM;%4+)\5h9q ߟn&ۛAN;E_jqsmdq3۞%| >,M%fcBBϥiww@׵nP +jX,4SOr̀սJܽ*E)b`zK!%VjnobRX,^r/~7񱺪z3|.$j?F!b3ŇEp^L fn/沊ۛf0LSuW+vw4mI#ts۩3sw\4V0l 8Mnj ҥgH}s`Kbk;(Su狀jA%s>it&/V=(3VPb>UwX܏3: 0|spQWy\֝a4Skxl?gfU]}?MrBUƃq]`*nnݥSa-I+?6B*8`&7ƻX}^V+fqY`&3kM-8NE<^&o*â6ivow댉Q\WE Ms'a8w6x8ƭQȳwB8Zb./XaU3P5mu}/Ǝq;3D^U;+D[V a ~ Vf*.z^-2M o5fl>1`m=ų>ܡteVw ։\a&8}q'o''*IIJ}鸻 a*]&8c 7@83R$[|ٙtJw۽ԍc&*x0yI=*8 [LIe<Ĥk掁?ªn-+1<pHԝ/9Jj}8dyD'7>2"]EfE"Fe"bN&յ:p݊rLr4Ӟ|X$4 ^txUH.rɫ~D8[!sz<6g fXD:7EOVVQl 6]4=C$ sf#a hj*MոWV \_۶"[FO\r8#yټjaSU"^ԓtYZ|033Y͗s3uaR p.B%_I{33@rk7s5: nf/ ۃF'=UYj30O nӆ.Ϳݢ`NQͅ:5I,G Y{uop5 V֐e9Tgſú8QutCްe5̾2=FNP X$M 'e*1PKDQ2PՖ~\۩v*3gMaѨ"L. p_Ǚ8Ip5magrc/FZݱCt[p<}5D7sea% I]Wc=V/Vݥ9$֒% fMԶ|]<f7fa+=f^<;7K^jT04EU .p^a;b{yypHy+f8MUp^%h(Q5Ź=YUXrmCS-YVz I%cx@칸v4kDhbUufp&sTp!ڨ'ЫB_=Z:%ݩX=˜ NϋNj`c8/\,c}IzL0V 0sqFxjM T((bja wK ̀8Spx,CL0&b6 {WqGEep7AGF;]W#rBJݢ Vx2v$cԢ,Wywdyi+qc[EkOi ΄w{t}OB6K:3]$34l@ IDAToM2g\*Q7sHDWF-zuw6ݘPp>wY0w;)KTbk{vnJ:1CSNY0FO=XU`\Stdk*1LeagE4D./~FA:%9Y+5.v`/>7/;mOqemWZlP+|CBo3{|e(K/m;R1/6NnM !VΘ}C4\^l{HYJH$S3 >ĎwfWըCw^ מ^peζ<Jlgb`<>(NIu&"D Ď6U`\Fq۷0& $Ŭ^663ϖ(5>0ow)b?L¶)%7f5WDTu03T%A޺L~IX b:_D[ƍ]ǧֈ?/U ^|J쳢 p&&o&(<1 s?@#Cf#@<+Veu52Ǿ nu!m>^7ř"&I:6gCX۹$"u?biz mkf^̃k+In=P ]Tfz` ޕd9([@LfJՀmc7j@XS.񬨯l2jGHY%UD1NE=0)Ld 6SMtkΊ`*/p:RYW] ]}?lBLSp0 )0Y~)k}WmoyJaL 8]¢HܗїRW)u|NiZxJ""^qrҚOcCD0r'.Y`++{,F,2cjq*z= J)i/N![v}9F_~/e*b乸t>>ΌJfe@$w}؎й F X-0sWiDϫ{m3XM}_V -R<$F϶G+Rwo:11ɜ &b85eJ[#ȾLB UȴszK"+p3D3Y5r8M:NKbps?Z,φS]숈{*J?8NJ5Є5}DOi7# uՕ4h)F>LSTXIk/ڰ^c5DY8VoT8,NQ:YխMh }Yg8ύxP'`ї䪟Jne|ds/7^h=s<dRO 7+|KTMً~\$eNSB /SϮgcXz됣:\"+/4EiGQx߾ǖbz> iJ3< };"٭}g)1RD Uw#ϪUL`*y"m̧}JO ޗFپ}l~}]4zIzEOƔ_[,MFPۄoUQ\z]bX `Ygs*(Qd>5¶^G}1bq_Ʊˬϵ(|r(p&"?U<-3ׂ*Ze#ٿTF^[Q6KDJ6m3+;S/O',5J}9$wˈ~,pw2a?{*<AfkeQ_NMh.Q %q 5#?[?g/ݱL0ߠ}}/}QcZϴ)>>27/1_VioIENDB`QuickRecruitmentAllButton.def000066400000000000000000000104021342332007200326520ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/QuickRecruitmentWindowG0      % ))51%" 5"0(5- ;" >. <%<2<-!>0!@&B&E(@.K+K.G9C4C1M1M4J;Q-U0T1T<Z2Y4[9A.!C2!B5)F;%B9)H4"I7*I;$J:)J;1U<$Q=)Y<"R=1a6d8i;p?MBTDRDUJ^KNA+UB&QA+VH.ZE%ZA)]M%[K+RA1RE:VI2\M2ZI:[P)]T5bGk[s@|F`O&aK(lJ%kL*cJ1kM1eR)bQ2cU:kT3jY5m\;rP,x^.t^5uao`7~i!vd6rb;{e5ye<{j=gZBk^Dk_Hs_Bn`FsdDsfLvhJygE}lDseZuj_~qLzlbol /75%;;)<);))%)%%);));;%%%)))%%%%%%A /75&));*));))))%)))&;)&%%%%% %) /   /     0 0 HzBt <n6h0b*\///zzxxzzzzzxxxxxzxzzzzzzzzzxzzzzzzzzzzzzzzzxw/H-%%%%'&*&& /Z-&&%*&%&%'%%%) /Z1%&'%&%''%&&%&&`%/74*&%%%&''&%Fa6) /Z-&*''%&'%***%%%%%Eq]% /[2%&%%''%%%*%&%'-osH /[1%''**&%'*'&&%'8!osv#% /7'''*'%%&%vunsv% /7,'%&'%%%%%%%aylmsssh/H^rk! `sB! %/7!_&Nrsi\s\) /H,%7_Eqs_]sI; /H-3rW%%3nrsG^rW% /H-#qrv8NN_s\\BQrI /H'nrv s ]rI /H%arl!s3QrI% /7^strk-s/QiW& /7,3$`r\3@-s/]i\; /H\rR-s3]i\% /7%]rI-r3 JiW% /H%%Qr\%-r3JiI) /7Nr\-iii|/Jilai\% /7%Qirr\-iGil/JfiJfI /H%%\iaDiW-ij Di3JlG JI /7%J\(GhB!` WS$J` \? /7&#!  ""  !  % /7%%   /75)%;;*<);)))%)%%);));;%%%))))%%%%%% /  QuickRecruitmentNoneButton.def000066400000000000000000000104021342332007200330410ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/QuickRecruitmentWindowG0     + )5 1*#,! 0# <#9+ 7 4*<#<*=.;3>0 B&E(D/K+L/G9C4C1K1L2H8I<Q-U0T1S<Z2Z4\=B-!B3 B5)B9)J<%J;(J;1U<"Q=)Z=#R=1`6a6d8i:p?NBTERDTH]INA*WB%QB+UH,YE&ZA(^M%[K-RA1RE:UJ2[L2ZI:\P*]U6^V8c@bNcPl[s@bH$cM*iM*cJ1kM1cQ%bQ+jT*nZ#nZ/aQ2bT9gY=hS0jX5j[9sT-sT6t]4r^8zej!uc5scj\CnbGseGseZ}qLoe1g|7}>oEsCuKx@v@{C{LwR{T|Z{R}X}HRuk0XFLENASZ]SYS[ZfpkrV][P\T^`dlkt|r{^`fktǰY˵TȲ_ð`ųlʴa͸cʹhIJpȷqʺ{Ҿj~y{~nu{{ǻÁČȅɋ˂ύϚяԃԌփ׌؄ٌ֒ޛ۔ݜڨޡminNormal.ypxminPressed.ypB" 0 0 HzBt <n6h0b*\/-/u/G7:;*&*;:*:V;*&&**:*:*R;:;;R;*:::*:;:***&****& /Z2:;;:*RR:R;R**&**::*:;;R;;''*&*&&*:****'''&' /Z7;*:;=;88;;X;*&**::*:::R:R:&'**&*******'*: /5NVR*Rk:;@qV*&':;RRRR:;;R:*&'*&&&&&&'&**'&&* /ZJ;RR:*xVR:;:*R:RV:*R:R***'*'&**&& /[K**;VЧzV;;;R;**'RR&::*&''*:'*&&'*& /[DV;*'ꧧR;;::;*?*'R,:!&:&&**&''&'&&&''&& /50;R:*詆;;;RRR*O*+1**'******&**&&&* /52*'*:詆*R**::*Pi:D*&&*&**&:&&''*&&' /G/::&*멆**'::::Qo©B&&':**'&*&&&&&*& /52*::;&*&&&:*PB*****&@%*'&#F:: /G3'*:*ϯ':**';:Pů2&**'**lo&^q'&&&' /G3**::ɿ**:**:*PʰB***i' /G2::RR˰:*:**:*QB&&&&*' /G2::R;ﴍ*&*****PD*'&ů*&&* /G3&*;;&&:****PE***&˰''' /50*&*Rഋ&'*****PB&*:*氋*::& /52*'&~㵊******R'}簊&& /G/R*>4|%D':wvv*UO받Oa& /50;*:;{󵰥E'鰴* /G/:&R㵤!::R촦r&*ⴜ& /50R*'g1*::::쵰*R鰣& /50;;**yM****:*'İ'&&g*&& /G/*:R**r****::**&'&pI&& /52*;::;hƅ''*&**&&:*R&&&&f&&* /53&::*;***&**&***';:*g&**&&*&&'&& /53&::*;*:***&*&&*:**::&&&****&&&&&& /53'**R+**:****&***':*'&&&&& &* /    /     0 0 HzBt <n6h0b*\///u/G7((+(&(((&'' /Z2'(&&&'&''&(+ /Z7(' '((4(&& /5N+&&A^($`X+'&&&&'(& /ZJ(&&\]Wz]+&'&&+&& /[K'+^nXpn]+(((&'&& /[D++]ppn]&'(' "" /50(&\n](''&&&%_Xe /52\nI&1nCon. /G/]naBznFn. /52'Hna1nnn. ! /G3Hna'1tAmF)II /G3Hta1tHnCfnI /G2&&Hta2t. Httna /G2&'\tb2t.HtM /G3'']tb1t.HtM /50&\ta1t!Hta /52\t]t&St] /G/& Tte .Sa]t]LM?1\te%1 /50(ptttr'Yttr. ptttb /G/#tr&ets5ptc  /50&4ts!jttItr /50''Gts1ptP4sr /G/&etDpvcGr. /52((4jM#a1   /53'`'6^ /53( /53    /   vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/QuickRecruitmentWindow/costBackground.png000066400000000000000000000251111342332007200306070ustar00rootroot00000000000000PNG  IHDR|t|lbKGD pHYs  tIME *iTXtCommentCreated with GIMPd.e IDATx}eU޷sϽ7o~IN fq<aRFB*B GHU%$KQAE!JƚFuHy0fJ0b޼瞳^c}c܆{,̻sk~)ﰠ9u|p5C_xԌ|ShE5hESY@Lc vڀ>oĻ1X-vE`4ELk:)!5GӑC-!Fޢc~YQY5@į0uG #gr"j6DcY+ o jo0GGmH88ß5",G¸vsOT &~.< zSA;'WX"G^PY/X-Bb#jp5 "YDbTMeQYo  1aKh{G5#/ ^lA? 1DE" DB$ І7Π|_kkGXoO+ܳV6j]F~.ϸ>&v$bЯ~]بEf@ B^LD"fsbx>%8Oh*6Skul}k1ô}ּ;Kl{xD|o}"xdž5-jϋyuk$58:88XxJ1jφ^{6ldH0ؚؚ yc0d6G3KV}$t!aEG8u|荾bw)iWqS6ƵŤh*bDi6&>/r6DYDѽ}Ux. 㐵O$ax4żK910/aM6~h1Xo*װrm=YX9uk؃i]D^CˑF]`笁 vaumE6VcL,ȃW4 ?4@;;Gk-=ȧq(5Z<# )/Jg [G#k#o F^= t"2`Z(a:dڢ1+v^BF#`-שA$l1x[ye¼K9,~ub6"oQ;eϞwOrMY؀٨*kᝅ 2d ##"X56d|/+Ͽ9xé'knO?oMjW.<#5686{uێ8< 6eDvIqumnu%1QʼFa xqvCy&6}Ei{FeĢcoT6{4%J,S"6YF#}LnJģMk&Ȝ59l=J]H\^kDo6c.R~H5\sG;,f ?]^uZ =9YH -& /T-keL]F¦GL@)bKib}䰻d(ul|PXCWo-FC.@iP2)$32Lzi-v\[4xg13Ċ 7UG<4 p p,>a-6h#|Hs>wB^=(aU<ڞJ8u/yU"pMR544[HyNVrV=@Yb%X]["s-{]L {۱5CG߲t䙀JڕCes(΂},b?o%ܙmI [#U(j!Y4lpjo v'}k6'7jTYl V!IXх6&̠j.$ _g ˈI=,~(Aρ%Z}bÈbX{ߗ5H~*5,j~aLjoZwf[?Ogq,w}:}Ϧőϡz.p(6ژ2mOorGc)e6RI2xks;c8LN7aE,z;wY}RiH4(fvۈ6P6rNJo*LmX!sߋhR۬U `/‡$B$$Vm%^}x;;,x=ί} lx u)AβBFC'c aRa}HemDةY._'kLfMuG)x!w)v>'[}g 1 D! zR/B8#5 uaVtmHi8ĮnZ&Nfw|F]ۯp @+!ʺTH7`єޢ \F6cC UAIT1v#Ju}^NKL}jƂ! +cCer`.n0@^狉ӚEM} v F7'rjulm|lwX .gmο>Afќo[݆.+Z1H6.Jf!mCmE5Y7t!¿jPCLI (H9w!Ҟ6K`Yc we'M#'$ Ÿr,[>Cv1Ǖ$ &6U7,-T۽e@)wglmhO/<ۜ? uϚ^n>Kz rNI Ma_mnV&h L4\<6Y6%6oylϖf4Z YP cjEd3p !Y pHj6?# vI*Wom1yx:ǟo#ڼBCtA `:$`7ŜC.%Bpן-x7x8"ʍD2zύ9!csG'5D^Mn|ஏ*ksJdZ[<u7O,vwWظE׷Ztged64DcKwʝY:_ / Hېd#3ۋƁٯx.E =yu1翓IT;siV5*{StRVF\(L*9=Ljp˘0\ L*|]6yxN?Y{Zv*i2a1z"ܞ,+~SqOå$9.lwiQ|Rs/w.=#2 ,9TCeJs|]lspCn[y XYb1[ݔI ~lj 2kǙo[tDW_9Caoy?~&ώ"Rf\#5wDf{taaKK3a1 - ̜{p,ZtlL6/荁Bp^%E8띉89h<5;msb+c{ĚN0PDhN9U{#a{ z5Kt!,mxL!:b1YVHٱ`}\;4O| rs쑤tc0nqoF-/|s)ZӃ|,3:I듨bt a)2C$e6>,teM9a͐Gz$ipZU&Fq\Lְqh$D{|1yؤo0e+D^Dܚ(QA-&͛ br%b5{RiU #[=◟FaUooO'Ñfˈ^r5vfmg"&\߉1Ys:/u::iuvyt0-e4+C=&%឵*+CT o{^yvi#cIƴHGv)BY9f RqF>M>fCܛ7GYS8wzn9?2G[1b;mYf#ԉ::Q^ːx,f)g@[Wg^h_2֜KM5?EHZ.(bh/!dCRc8LLDXo<&5=v5 QX@*|%5%JF-gWq8Νޤxš~hl㡇HMmsZN&; tZ0,jcK#k?H4s@+Ȱ)JlcPiDߩ:2H\9u,3k 2oŇsgAX&'2q1:Iᜁ<0IlO$ga<$,=H4f@`y<O<ax]2K`Yk7Վ{B袟-9'O*ƣv2 RP']:N-`x^rj> gযs #Eg>x/PP/^k߻|sa~꽠{1ǎu)D1DpS k䰘[R `a Z de^k=Fe 䘲ImfTk5M7 Af~@`xp1=a n~%v rACzscO \"w <hA2&7]-}wU޳kכ@[ @䈯O_}fև/5[h^$c= 0cAfB8EJ0ޠG 17זeqaXH>M[ Ȉ[4Π1]t޲F|>K`!P,]@PyITW Iӑ rʧa{NmL|rkC|-Lͣ0-˕"XAqOVХHt[c!*$ְӛa*@pM6b ߋv28{[=v\HR1|-=0O)K=v"亪H eI@Dk DzϽ 65Ί:.>X{[!/DA_P$J6tnzcxa$׵Bf8~}Ц!)ve 9$ KIHs5H<2#w(Hxcc!=L#dxʲ5]`aJq?aW 5 3ݘpc㏵7q/agũ'{߲)?u4z0iMklkIKY`+džmCA +ZV@@J$Øڍ7+^X-C@HN0!5E5R6,siR"p)٩мavp[6S'O8,Cq㏵xO44n/6 gAB7 鲠93-xX٭񎬬 ,RTٗzБ6̢@hTeZ[KCt򻵳WLx)l{y]"IUZPKx9uhIBU  #?RRDyooe쩭D GSӄ0{s\I_Q X8v?wXȚ.} MZ}0CCPyZZ#ͳr IDAT0<]ʀdm#'|oDCήw"#!6.IW^PS}9 !dHJ& ֈA&'6BixĻ_|k{N O= ;\{"z-C#]&}ca(m'Tċzʵ 0:NZi}Z(K+=:ydh|,:'/&"'D^{c[ѱU$G9|\ s9~'(i b_ Y x]vۈE2ABI. <I̍ry ZJʆWtX|Y,'jp)G,_szb_HÀb%@|#ӣ8Wsqr9@c[SgF[J|G^SVHvgs+lX=KJ#1ZL0Ab嵕3|=~"t1b)3o!l- Ĕ>qY< V#Xw1 !߁nԙ[rHWqv_? aEU#3xD$CxQ+ #TIG ԔNh}`hr3oQ_x #Q.kGJDE,}$,^,8W0ӆY!eMRrӌ=[eukL )}*M ٕYQBy#MueC5df3)$|t]?~_| QFaaWeWJ`O^PCEsI}P9%ŬzѱJT#0X^ZUꙋeA}C70'JEcJmuxֱi9)faVb|՗yg=43``8]7b-'r$)!,"h i8 1eU&r޽79RhIPy!$oZC^A5r;K5(Tgxi,k!i߳p[O"2f gVbG>z\_bp|cJx7Sc'?Oe=Obta;GidZJY;(@yv az]H Dků)c%;[}%G,8&> g dIϮޟChERԹ)Zǚ XlGOn[~z#nl»/|<J7geJWaA;5C^Jܙ(l kgك"­Eڔeg( mZ操[d°( dzl[W/Dp-|x1Yqxă>/^:/]Vr I͋~4xe",O,R;##g@Ąe4z%="wppoHN%)W^ gN}B,k,1!=o?׿?TKCѶ-gJ >|sed'lNJ/>s",_Ϣ;!C,(((XPP bAA1Ăb !C,(((XPP bAA1Ăb !C,(((XPP bAA1Ăb !C,(((XPP bAA1ĂϷ;(_ Bð݉IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/ScSelC.json000066400000000000000000000001611342332007200224500ustar00rootroot00000000000000{ "basepath" : "mapFormatIcons/", "images" : [ { "group" : 1, "frame" : 0, "file" : "vcmi1.png"} ] }vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/buttons/000077500000000000000000000000001342332007200221415ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/buttons/commander.json000066400000000000000000000002241342332007200247770ustar00rootroot00000000000000{ "basepath" : "buttons/", "images" : [ { "frame" : 0, "file" : "commanderNormal.png"}, { "frame" : 1, "file" : "commanderPressed.png"} ] } vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/buttons/commanderNormal.png000066400000000000000000000062401342332007200257670ustar00rootroot00000000000000PNG  IHDR\&~ pHYs X =tIME /`tEXtCommentCreated with GIMPWPLTE             !!""""###$$$ % & & & ' ' ' ( ( ( ) ) ) * * * * * * + + , , , , .#/ /! 0! 0#0$1" 1#2$2%3%5$5(;)<*0>1?1@1A3A5C4D3D5F5F9G9H7I=J=JAK>K@L<L=M>N@RDSDUFUGUIYK[L_N _T!cV"dY#h[$k]%k^%ma'mhdnhdoc'oiep`'piepjeqe(qjerkesf(skeskftkftlfug)ulfvi)vmfwmfwnfxnfyngzng|q-t.v/y0|13455689999999;<<==?@@ACCDEEFHHƻJȽKLMNOOOPPPQRRSSSTTUUUVVVWWWX'LbKGDߞi aIDAT]eWY:{wk&`M-iP !6*HMIhLJ[lS8 UBh#iS-JB0әZ{=OZ @.GͨPx^d1ȨZnœ:"|hJej{Xǃ-Hͩ:1*ÚH#;ewFԕI`aq161wpҁkir_b}fN0sց6z:u}WI6;I6[W# ][- Va3h swj$MKI2F aNyZgcZ}K(7iA x JbJ_gJ C GAkq̎ȡOؽ\77 }[?iNXVkVؚj+o|׏]g.J=S/t~}hm')I3@gE, ';l[zNЎ{ *:ؾw(ˁXd;W;y @4P<:(؜Vl$%| 1t0??֔4~Mr2ъAMUYYW9k{+ =KnZaB\~|Vy9M<&]zTB١ЮWl{a,m\(C&+d%;Ȗ- A>8-¦$jۘ,|a2vn8sfBQY(&/?-!8x['bЂ`@ߗʪsUh`G-0N-&?~"WE꧞ 8TeJ2INY:,0U1K"jy7~ *7U ˀ1|FAX@AW0I . T ӂyi*h/~_r̷j ~zZMaʑi?V9%8 멑>aj%ľ6֭gVr|%^8ZrBCo{ js: dH7Қ9zT>BE/ ZKImQeDzT0`@n&f^[sTgG/U%zKOZZ*@(J(oњ5RUkjWpa`~;VAβXV6)SX:1o^h8/^}bzL5붟żwz6-hͤ qr'7ғtp|~y~-5=c[`]7HrRMMEEZ6J&RzC/8s9/>:sq]ݼV\ܠ;04 qULӹ{h U(4hRG^_:`!vݵ[k3Mۍ辇pKdzm?v'i< ?+ 6 [b9&2@e!f{#k$iwd*U2ֶd;[ܧ.=7G]lcldG$Y *3us\8rᴗw99~4&Crph~*KXb=m! e-=8ܯDi{䴖mOoBqRZVj,I+j*3J{ҽo? =lÌjcU+QjR]H4ל-fX\$mxџmǦ:k x.EsipȦg k0R[@%KI, .4{(^7=1/pp;GrMM87mYGMB(& oW4w>Տ ȟc=!A??O_/$+Y}bbx*R[P0Wk;>=fzP w幾)8X%ŐE;#&h؊u}}U[?uC*K[&Toz۾A3(YSۨVF!oxSh7FZDww Z^,u: WJخfbtaʲ03zDIUѤ`?Z @Ӕrk\:eۤ%D w=:ZZg˒ErdiZ]U6L65_=H]'&q@ śN458148jM@c8p C܀jN GANhFN1F09DF8Af>]uIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/buttons/commanderPressed.png000066400000000000000000000054521342332007200261500ustar00rootroot00000000000000PNG  IHDR\&~ pHYs X =tIME!:ӰtEXtCommentCreated with GIMPW+PLTE                     ! ! " " # # # # $ % & ' ' ( ) *! ," ,# -# .# .%/$/%0(1(1*4+6,7+7,8-8.9/90:0:0=4>4?4@5E<F<G>H?I@JAKAKCKGEMEMIFNIFNJFOFOJFPFPKFPKGQKGRKGRLGSISLGSMGTITMGTMHUMIWOZR ZR![S!_V"aY#bZ#d[#e]%f]%h`%kb'ld(md(me(mjhnkhokhpg)pkhpkiqliri*rj*rk*rlirmixq,yq,zr-|t.}u.~v.x/y0z0{1~12345567778889:::;;;<<<<<=߳ebKGDM&`IDAT eWYc6:&J1A5(8Cʭ:zbOONOp>=oyǧwNwONNɝݻ''wOO;>;={޽x>=fvfsfnnl6bmv=<=mvnwzCF r @r #,a?Zbby.NwWfu'Dh%E`ܓ`?hI FĀ?Z`b\ QYBqw @ GT?_A[HJ^,M%# "?X) [㋏ޕ)zV͓Mٴ.T1a $'[k#1 0AkaCgz媒h{aHI)i4i0ܢ[èvq- MB*z=zՋk> n$/s mq5c4HEl?0@^de:YYš5C˳a̋4 $KZjo3#ҮZkJ"EV%yN$Uם}EW9;#mk_ Hc{./f.hm{"QRT@$xrFˉ#޶|sǔU[׈H%}H}l%,'2J-ybsk߂|cE^M%%ru/|Ya2@$0?Ю]~Rl ) '9XUTc*}@ m <L%YgM2_owd@`u<@@!ՃK>:2 /}oj[kWo~eT ,g;[ ER|hR-J]Ͻڮ}" $EL:Ahҍ}FKvsJ,cE7~};'<׾aD2z%Pp{aoEݨR%wU!"CkKNx퇧ɉц("Cr*AW4mSOAD23$yq|[\ݍ^[ Tut8z2*d0̊dId꯵}{="5+\fZڪϟGHT\笟l~Z7|ߴ~-~aIVHwe ]w|1ao/ sݒT^mU{wⷤ$ٔ`szaEFOwcu@-j* _<oK~-/"'$ ^50/auDH1'/=D)So|"s=qT\\D@-IhtSih!Ĩ 2$cɱi[vv<$ `u ,^)K{]"R4@,\_LwHb<%#H]xu{C@Ҝe2KT$h 1Zk;T %}םlBB@JR$>dZD_RD!PJ 'nKD!Ҕc`4R A#r?fFE4IPCn77n͛UXW~W}\V;_[U߯V}߯k_/@ IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/buttons/resolution.json000066400000000000000000000002261342332007200252370ustar00rootroot00000000000000{ "basepath" : "buttons/", "images" : [ { "frame" : 0, "file" : "resolutionNormal.png"}, { "frame" : 1, "file" : "resolutionPressed.png"} ] } vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/buttons/resolutionNormal.png000066400000000000000000000124561342332007200262330ustar00rootroot00000000000000PNG  IHDR\!TSGbKGD pHYs X =tIME(=>tEXtCommentCreated with GIMPWIDAThuy]E{}C21$!  (`A L"(A@hYhcjek7FhP\BZZFmAv`0() T9>w޽խSo-g8QB&5`DP/A;bE0 ;A$(ދ:UL րf/ x";͞LOi5&[rJ >%gdΕTQ_I K5Bs D.״BGAuFgJ{3y6}ұ%89QHΕn("4wSW>T4jg&r^Ɍ0QyŃN?!- s,9Y1EF D|ERK߸K*Y WZ)3}9jXm&KO77xUL<{傐FlD(tJ'45w+yԇA; B3"aM@+Z{_E $h#Bf5kLlb0[ OƃEk%a%˚52VBW2+ta rA|xvcg ߯"x |6%Ntђ!HXTcJxDZB'h*נP!QZxnn CKncA ZOpLU^[JtmTŀ x?*ZV`$@K}Fpbd#`*Lx(eYR iaZL1GŊ&ne Cl(d1 S0]]Ą^:JTBw5ADb RʔDú}xp' '`aMoD:;KYGyh-,zW bjk V6 D<*G5WmWDQ >BEf^fZ|Ged1UMlk3, 9ݺ$5t ̆,$zjoӲ& {9-Hs+8Xz(lēD5DȤSIG |𦶀ĒUx*f)ʟS~`-y0N5e\XY̘gH+3(ZTB %3 rcjV:Sn utOb`'xdCJG\V7v"3늟;bB /iJ&1_Sf| Jt{C15e5WP1!@^: HL%=d;Ujh[G}+5KVJUy%ς1 F塓LCRJTQ0)ɣK'\(HUL쥁2BjPn2}OV+A5TDY7%YPuP$T Ԑb(cx JUA3Ry_uqe׭9}˒E̜1˯k'v~*.vtRPXnGn_},{GzWG۷ZYp7o b8K)rރb~1~~ xq^1ssv} ccrWߞÈPvpчesX+fauCԉ5?vvs֠f{mg}࿾g}p]8ų eyZ ^_S!O? V~8bX[e%!X*KcW f7q ҏH!7nnX4e~!|LO]̕\?aX3 ^X7|f^\ғYa0Q7j"eXV 6_ǴX8&tж/1ih~edsxwȬ`{t묥J^3!tӶ,Yf}/5 vlN?y: Bp2`;ҥKKɋ"%hצ@-Yf2"t-w!J@8ɺ:M_V+=o!5dmB&<²pl@Xz*T^;0oCL%s,A0 #>NTHr3O9ko&_u,X0:l O" #+/gd)'eȥ'j_^2Y9q&'&yn,U[N=ǤAUtSu&g\9_8>P6Tw[{5kMnmXOU!$K6 gXsCF_칄[oĀb)V[mɁٟ]8 (_w3gL駟Y`T)+Ex.¯\pαǼ޹ _sxѿ:rCG#:UW}|ׁNl#_3Dug零<31l ze9PTҷd*Lyul9.DT[bbb"2o1ܑno1 *Ԉp⧎gݫV[1B[):ZL%U*V?^Hp!es}S+u$O?4!-͂rZw?=W~q9(W5̞t@ȋm(?Ei' &L+|0S@[ӟ\HQQzVC!;opsp^;;y(}*ԍcS'd"'BST^mߋ fS kCsLT+L+ּj"/ytljH:_Oy[ӎ[2I R"GAؕW\y]ܳ>±"׮,X'^0R3j5c, (&1{xGrfE ,p570q-&GvB5 笳. k~x]rW̚X ҁVNT4NVy\4ɟ<].Bk[3N9Qw^+_9PF2H6;lG=ffk3<2$>02?8su]A,m{[|?Ϝy^^?]aȊc{7)ё!>8{2{lp}#fR~~o;Ȭ-( sgx/ribp, ~8C{v̞套2<ҥ*=~A8~wgMlbS/Eŋ7sn[X/é~3W;-ٕ>& n_t@0Bk> o-xyckSH@735K6=} e"9ϺIG;YY, wѱ~jq4Ba W&JPеѮۚ(}2#l1کydSU[?vrcZu0[FW+E"-6B]y93SV iab&K -5]rHZ:6Jއvsc F9M(き@leyƌ^F/35r̟ayÌN0 TUC s+lǂ" /}(mȖT떝H;tkPH'F b5}eEp5_٘4[GLSjMӽpЩʓǞY d}a׺9]dF;Klh51us]WiTlX7o,5qC~>VN,]dDVSb:S(Q zJR^cBk0'FB.5- H`ԧғxj@ִh(rK\K ^/itƥ_% XE]ʓŻ$~?uuD'C0nXC4 Jyvn4&(ԼZe`~DZ5s-<$<_-5zSXr(D=r4u?GH:)]pq++;*"BPǾ5}f})`EPw [+1aɝCK" T gdTo+\8zmitNB++jBT: ~0"F?cDo*W+BY)cZ+ VLDkm~ Ɉ3*eM(Hz^{M .,\ҭtya$+_yAK13ԇ(+1Ba ˜^Q\qyJa\m.>hqt.R¡]p1p9 SzA* XX{ ׏\[d/m* @M< h8 ^ɵ kTVj:ƻfU ,vڳ4D~*6E#13p"B\FYúI إ@RZAZ)xyV"B%Fe_).Xd/e% X UV.5BY1FAn xqѓْg1„ TKud\G$3*# ָP*A&.UATb+烧pn? . D?^zO g0'ߛ `-k2=:`"UE5bmD댊rAP rA"+M/Һ4WVh&`h#3\]ELV֤`Rì1qqI4m1$h"h ]9=e |Ld,$@J|߲´AeRf 4 n_%c3jFb_$RJ2iBaEL# yECYi#QJnB'FkcoElGLw+׈HrC;8.Rm! {[f1,Bԁ%Lte!K(` ʈI_1Ԕ]tLkS`VŅN4"O3` F %L.rn96Ԑe>rbl,fl JULF5&Q m5k+TOe6*b=G?u {2,H9rpCٱyǹeq\3Çxسw+^s:V'mr-LLnSʢM~͏ s[g3V "L?ħ?uXڣ{ױdbFGF]y=y§N9{ZVv*38t ӌ q{96mzw?yÚ5+Yz9~Ǿ}QC< gק|4m@a.-sy%_ڃlKs.gUgmpE~~?& Xǰq/K:m|+h[amSpڊWp9 +t q~l'7ӇA {;k2Tvռ~UTU-~s*d.]w,X~Ǯ_}v9s),OU7R4Tpj%:]v*.k.;aŲ%,9uE@eGc: N:dwJq`Ur¬ wX }X6>c-掶7f='3ySx>ZR=R0o쑂#pe7a9V).`eBٍ?Ɍac qo/aSEL Lvb*xn+b: Gp"‚Yx!?ѣpAo2.%A60"L7âEN  k.zlffv,cͅ>G&.ZuQ7 O/nQ-t ǠY Ν(~Zetfdž+'>-kc i]`IG\wel|` z_s_żOdr4x#2<ګ/aũ!|@ #бm?a\^Ͼ-9X62fu >Woᣟ-=\0'"r1VĨQ NY#"~̋|ƯQdʕQF2wV[ {Odj޽{9~ s(E_{7(w7+gzzrKbO2kD+y-Ź\Evx{ūU m E!wPs^)ڈ JEpS4L^p>Sه>7{H5W]]uʷٲe;3F,eOIl=U5TUv;^eA SS=nņ o]^z L8ȎOwqWq͵ܳ/_r~TW-yiv<ųOpƪEl;?Ŧ\=(iy=/ݺmh;E`1frrɧe3<.tkV/d <$Ƙtb疯>ÛP`^<^{kYt)w~[*3ʌwDb`P:0k`rb_ϠSc`r$t8w:aFdž@ `bd\2ֶx`SXePu+Gt>X. -ntY˟ף$kT څ6'J4 +¦peEi b|-%+k:gU5SxnCa96jPmf YK_ˌ`ɤio ZXr5\l/IP M eFZ&U1c&qZ֠(-X$F8gzn ;m9ĎHnz oʬl42P:JԵvBm#}ǩ֥ C7W]fDyN[sѬd(|6^r"RCӃ i_"̾ؒu^b$ަ R5 F,jL]Ь0JyCd%XZinuKQI<ė228Ծ(GīX>}E]Bl7E GK?R7M0]$OP\u@Y=6 t`%h(9RK. W'pu)^ `rz*z0j9yNMˬVbҴRPnu\ېi`]9=j~N>dt#ƀ5c?e}R9ﭕ)xFlt!^#!niRB`gM*ؓ,s7ʎ\5ƀF%A0@2j:Aͺ)}*ѱq\9ɼOC˯WdȜe8R 5b"9D?$&WjeRG&F*RkY?;9zJKi9mts4bQ-u/Sj6;g.yoQj< L'Tde*@ng R1 ɃF;ߧ7.AR\E(0cYi4AyW&2l_K:XObrZ7zUЊKC0b%R,[HSl\J7:yiN֬D5k`I'.Pxf IPPIuL( m8;yxa-#LdNP 4Mي6FcmL@sꦛשP?Ho֣* Gd1'R9S[~9x69/2 \/lIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/itpa.json000066400000000000000000000001551342332007200222740ustar00rootroot00000000000000{ "images" : [ { "frame" : 38, "file" : "HPSRAND0.bmp"}, { "frame" : 39, "file" : "HPSRAND5.bmp"} ] } vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/mapFormatIcons/000077500000000000000000000000001342332007200233655ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/mapFormatIcons/vcmi1.png000066400000000000000000000040051342332007200251110ustar00rootroot00000000000000PNG  IHDR P,bKGD pHYs  tIME 1YGIDATHǥ{\U>ՙ}NRvK[DA bЦEĀ(H! "&`1?P^h%F )tv vgg;snm/99'|uRJ!yMJFxONNViVTTѮV5*W@gJuuuX"ɤ3t[.Ֆv3mt#LЉSJ#%QV500M!uw/YtiODQ8 ()%a eDDaHӱVR"p=rȑL&2 (rttv'yOR*e47GsFmu\EXyEUT J*Z19YdL黄Wb @(2 $K 3iӖN3>5͌ЖP*M beFfalxװbfb&F2as{a膁iO:KvJ4Mct~h\1>11ݳ1 BC.RJ4cS-[Z{)ˈ̖ h+d{$nJ''yT8oP;GBO>&ӣ=|ON< |A71i.i,HA0\ϳ]q]w2HBEHbYmS)[Z\]˜xϼlx^/Y8vz !1tYGId"AxW<CqJ!l'O>ά#}bxv 4݅7`>:jMd A(ٽGls #pNŹzWB&k뺖݄ @?%8q~m]{y/M<zd==ƉqLC')86Cz ߳@8MAuf:MA_J))OFόׯ{vُA| כPqghoːb:b}fӓ0șmKq{@kEhHoٲyT$Q$A)4!5Py4luxs=x[㇀}@C)W)VJI T={^dUXNYaSI,qb/lg' oN]j Jʏ{e%躎nskcLҙ 흝|Mܹy3](MOZ:| LRxŗu7LѻM 韮u]'r<(7` vs3R=8p\~:Z皍LmwtuevzZr/5A_"2Ss 0Ya#^E,zc'? {u_ƭ_"H+Ye&NG$R7Y 6޴l3gΎ~< Yy4ົn_}(:\nbfsrt+u\e!f,F4XfYyv /R  9IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/000077500000000000000000000000001342332007200227405ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/cancel-normal.png000066400000000000000000000024301342332007200261600ustar00rootroot00000000000000PNG  IHDR.ņ pHYs X =tIME2"_tEXtCommentCreated with GIMPWPLTE            !"### # $$& ' ' ( * 0$2'3%4%4(4(5(5):*;+@5A5C6D7F8G9RE(RF(UH)j_8k`8la:ma9nc:nd;nheoc:odl8ױ u9M^}l?!AncOvϡ"XKsfxpvˏW8RvHr W?}oln?e sʢhϤTK~ 8OE8^ awnyd_vc}z9TA& ?5D8A8 Ap;)! `qq!3IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/cancel-pressed.png000066400000000000000000000023741342332007200263440ustar00rootroot00000000000000PNG  IHDR.ņ pHYs X =tIME28=tEXtCommentCreated with GIMPWPLTE          !!"### $$& & ' 2%2'5(6'6)7)8(;+<2>4?4F8G9H9QE'TG)VH*j`8k`8lhcma9nd;nheob:oc:oiepc:piepjeqd;qfKpU J0i.,zg^z}jhf:Uccҹ{t۱VvY͵`Au$J) F[gV^=@,C*`j0IE)'qW6ʻO/o܉cjOޭCM,;L%X6ǒrWQ[ƴ^0Ţ~qUebd܄PDOR./._܋keRo.ya kjqu7M=}D0TFLvk%\x)( 2 1q iWζ䃭o=eQ[ˠM5e]/oP2::ԩcQh oϗw~]xR.9!&0Kv{3R t?hlIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/cancelButton.json000066400000000000000000000002241342332007200262520ustar00rootroot00000000000000{ "basepath" : "stackWindow/", "images" : [ { "frame" : 0, "file" : "cancel-normal.png"}, { "frame" : 1, "file" : "cancel-pressed.png"} ] } vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/level-0.png000066400000000000000000000002661342332007200247160ustar00rootroot00000000000000PNG  IHDR*,*FgAMA abKGD oFFsA -@ pHYs X =tIME  gIDATX  Om EPIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/level-1.png000066400000000000000000000023661342332007200247220ustar00rootroot00000000000000PNG  IHDR*,$DtgAMA a oFFsA -@ pHYs X =tIME  gPLTE     ! " # $ $ % & & ' ( ) * * +,--./0012233456789:;<<=>??@ABCU1 yFb22U1 yFb"Z7tRNS ;bKGD;9l`IDAT8Oz6`]'뀍 HtNo*3~+7?Q]_ak9Ǿ%Y*>joT6zFI :܋%wVZ R|zrPFc**l~ȳ'BQ\HrY@5{M d-m.>90LqMƺyF&n5ϧ-'pAngs5X{Y Y~5$9ݗAFk5o=6'(b6Ƒp MR9d6Wdk~$ګlg1UȊ*#Q(!~^M$먿[t^?vji2,?%}YȆ[oIo{7>V p$)*?*d-}XErI!"OsAUd~#/Aن~ñ[C~E Ao1}-vwЉ\sNǾkh+!2uJn(?ZJ᢮C[':xIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/level-10.png000066400000000000000000000021121342332007200247670ustar00rootroot00000000000000PNG  IHDR*,$DtgAMA a oFFsA -@ pHYs X =tIME  gPLTE v  ɬ v # 0)eV  j[udUI D:   $в0>& zٹڀ˭.`R ]WE `N s\w_yaklvw|yJɬвIѴVҨժַ@׬ ܼܽ[ݽݿ\G޿\+?^9<@AIGJLNSU`WZ\achlmortv{}ڀۂۄ܇݋ߐߒbItRNS ,/5;bKGD`zIDAT8b```%5"UBXJBQT•"Q2[GY%\i T{ߔɓr Y*c`J=j=[&UNRSbUɀSyd 4qB_o{K,JZfaI@0J ]e0\>y }=b3QW8E5\!j9M[J r3]őT+XcseQnfZzzJp+M6tH @W /#'bP@:"E!IXT"LH{780!) `7hdzc@(AH)3ݩ+gYx+kgjc`]ZF^]+ӵ]m,h(IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/level-2.png000066400000000000000000000025051342332007200247160ustar00rootroot00000000000000PNG  IHDR*,$DtgAMA a oFFsA -@ pHYs X =tIME  gPLTE     ! " # $ $ % & & ' ( ) * * +,--./0012233456789:;<<=>??@ABCORRU1 yFb2U1 yFb:n09tRNSbKGD=mQYIDAT8Oz&`:-$1MI$VrUd>y 緗m]N h+0|0hF=[`R>+%P>75E^7;w!`"3XN;EoW9pŚ]~tI\ Eic7XAHwrH 'X#y F#l 3YcS4.z~*=p%5lLagiܺ~FT91hLT~ b#}`gew/6mzc.Wsr2WyI+6Ff0e5t56I$Odr2^dft_Y !Îْ1۫6KAt![JWv2 |3K9H7o*ڪRFnHzx?tym:XrSd0H)a=@3-f+7uDJU,v7YX̞Fqٗu\&rǀq&﹇ N&yq{_o٥w~{??@ABCDFGHKMNORRS2U1 yFb1 =tRNSi8bKGDAlNIDAT8z853M˶ʖ}tֳj.ꂟZNk|/p8Z[O ֯/bˏghsK!}J0n|{s>/ I1uܯPBpx_W *je8wPhhbF5y F9l+'B=yovy>XX[9||#F7u0_djcp%c) „Z`'.dc߉B =Aڈ.# l&h-`$P؀8cIc1_\ E>|O%Q%`0LX]fQHb-eRS a0~䕒:Hmn}K 8peCd-ҫ:31GK3pɹHˑZd楰{RA!\&SZH՘̼Ey"rdAGvj-*GӯyڡoJ#Ogd}-/ ;2vim.1o3t^5?]2.=o=fB kgg &qVo3hw%nj0Ņ\Ζ|K.N![ӓW3/t02S^QPg9-*gbx=>V#ZmXt?*oYGÝέKvM$M|h' 'Me b}}ӾdXt53@IT=Z1Ue9Z1a4:yp]76^t#'J{Ļ}GĻ􊞃2@B1$<$V{HI?i~l`Rs0?GApIT(*E `IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/level-4.png000066400000000000000000000023661342332007200247250ustar00rootroot00000000000000PNG  IHDR*,$DtgAMA a oFFsA -@ pHYs X =tIME  gPLTE     ! " # $ $ % & & ' ( ) * * +,--./001233333456789:;<<=>??@ABCTTTvvv333333TTTvvv*Q7tRNS ;bKGD;9l`IDAT8Oz6`]'뀍 HduxVa]i&z{?gwer'QZF8N}%K%^U|GިmbD+uٹIuK$AJ:6,eA!dB!@Ҿ Fip#1g5Ab61Un7@|Vt3q&;q46~9N<=o98rp=[j^C%ɉ F6Z~Q8Gy4[u^h)h$a [#I^e=^@V dWI`B1 zm"YG]ߢ%XgV,Msdi.jE6rx;$vX-l>HRU~ >CUڭ9mݺM*=Ho=J25~afI iqdi9l>.}XErI!"OsAUd~#/??@ABCGHKNORRTTTvvv333TTTvvvd=tRNSi8bKGDAlNIDAT8Oz&`:-$1S8Il;=l['vrWUd}j?` ֟Ŗ8D3gw0[ /O3@ap:rS$T;N'/69z<J=e]LJ5䰭t䛅z@_W|>XX[9qbdpM7`&8\b0![ȅl;pRxzvI`C4AsmMu@)c|9KL " /- /gr tO]( NrH-LcKWJ ާJNjCd-;ni}گ1[Kg4Y_y"-GJks{RA~R4ZH1μ%y"dAGv-@mVei `Dާv.[2e{drw>gkrWik&Mjm^ekRr5e^,m^{̈́v?TM9<>KZ2YFghhۖ: "sU4tYUL_B)ʺ1,XɣqsݭW,0;W g /}6ؗ~7ZvdU`UXBY7yWN4;ύYfɳyhIe[ .6eI:`i]nQPg8-Uf@XAghb9;deM[ƅ7]c|R6GF=m6=i*k[1KLKf4H9)Ţ=6bh puYczN3Oqx==WvcH#j]2@B1$??@ABCDFGHKMNORRS333TTTvvvH;=tRNSi8bKGDAlNIDAT8z853M˶ʖ}tֳj.ꂟZNk|/p8Z[O ֯/bˏghsK!}J0n|{s>/ I1uܯPBpx_W *je8wPhhbF5y F9l+'B=yovy>XX[9||#F7u0_djcp%c) „Z`'.dc߉B =Aڈ.# l&h-`$P؀8cIc1_\ E>|O%Q%`0LX]fQHb-eRS a0~䕒:Hmn}K 8peCd-ҫ:31GK3pɹHˑZd楰{RA!\&SZH՘̼Ey"rdAGvj-*GӯyڡoJ#Ogd}-/ ;2vim.1o3t^5?]2.=o=fB kgg &qVo3hw%nj0Ņ\Ζ|K.N![ӓW3/t02S^QPg9-*gbx=>V#ZmXt?*oYGÝέKvM$M|h' 'Me b}}ӾdXt53@IT=Z1Ue9Z1a4:yp]76^t#'J{Ļ}GĻ􊞃2@B1$<$V{HI?i~l`Rs0?GApIT(*E `IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/level-7.png000066400000000000000000000023311342332007200247200ustar00rootroot00000000000000PNG  IHDR*,$DtgAMA a oFFsA -@ pHYs X =tIME  gPLTE     ! " # $ $ % & & ' ( ) * * +,--./001233456789:;<<=>??@BCWE y׬ `WE WE y׬ `6tRNS DpSbKGD:N GIDAT8Oz6`]'`%&ƉXeݯoЛrͲ|ΏQCoD'yY]t$AJz6.좌UR2#g }/ NFiI[k *oD>t`4e:堑ɱ_V8(׷毅,?P(e 2&m'Q&DZs9hOv­&/4Ihd6$%黫la`)UɊ*'Q(1}^m"i [v^?#k,vzm2"?qZ^G!~#;|6^DRU~!CuؓudM7Db\ ưͬ>d/̬ [ NI,O q\~e|L>/GUjw}!vP9)G_ֹ]K0u9deSeHLޙV Nہs_[GS4J75UW'~ ӥI.F64 @w}0`l70(8iAP&Ӷ#?` @V{!&4  M\nkr_;;ز% 窃58\C5o~yY.}7aIé*8b'-0{vwЋ^*j! WZ~<~@EST%)gѰ@y<IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/level-8.png000066400000000000000000000026301342332007200247230ustar00rootroot00000000000000PNG  IHDR*,$DtgAMA a oFFsA -@ pHYs X =tIME  gPLTE     ! " # $ $ % & & ' ( ) * * +,--./001233456789:;<<=>??@ABCGHKNORRWE y׬ `WE y׬ `%=tRNSi8bKGDAlNIDAT8Oz6`uF$t>raƻqrWUa)j?/?=-?~ϻ8F3gw0[ /O3@av8ha9NntU?N'?'U^pL=N?B=e]LJ5䰭t _rg}^:j-r*aF7u0_djcp/c) „l-kR=VIHO61e$ ͵l4Y hp,=0)s38g@Q>[B^2 R -ٻlQHa-eZP jl'AjѧJNjCd-;ʷniz1[Kg4Y_DZ"b"RiNWcyJDd " [l,gO]d"ɒ'!|2|/Җ j[M,gۼ *b֭=|ya6yCŕucY\ǓG7!X]s}ӫjBݍ+wY~]-j ZsvdeM[ƅ7]c|R6gF=m6=i*kk1KLKf4H9)Ţ=6bh puYczN3/qx-=WvcH#j]e<cIryHZmo` WħF K;7$j(6/ TT0%&mPM1Ti )M@(IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/level-9.png000066400000000000000000000026551342332007200247330ustar00rootroot00000000000000PNG  IHDR*,$DtgAMA a oFFsA -@ pHYs X =tIME  gPLTE     ! " # $ $ % & & ' ( ) * * +,--./001233456789:;<<=>??@ABCDFGHKMNORRSWE y׬ `Ow=tRNSi8bKGDAlNIDAT8z853M˶ʖ}tֳj.ꂟZNk|/p8Z[O ֯/bˏghsK!}J0n|{s>/ I1uܯPBpx_W *je8wPhhbF5y F9l+'B=yovy>XX[9||#F7u0_djcp%c) „Z`'.dc߉B =Aڈ.# l&h-`$P؀8cIc1_\ E>|O%Q%`0LX]fQHb-eRS a0~䕒:Hmn}K 8peCd-ҫ:31GK3pɹHˑZd楰{RA!\&SZH՘̼Ey"rdAGvj-*GӯyڡoJ#Ogd}-/ ;2vim.1o3t^5?]2.=o=fB kgg &qVo3hw%nj0Ņ\Ζ|K.N![ӓW3/t02S^QPg9-*gbx=>V#ZmXt?*oYGÝέKvM$M|h' 'Me b}}ӾdXt53@IT=Z1Ue9Z1a4:yp]76^t#'J{Ļ}GĻ􊞃2@B1$<$V{HI?i~l`Rs0?GApIT(*E `IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/levels.json000066400000000000000000000010021342332007200251160ustar00rootroot00000000000000{ "basepath" : "stackWindow/", "images" : [ { "frame" : 0, "file" : "level-0.png"}, { "frame" : 1, "file" : "level-1.png"}, { "frame" : 2, "file" : "level-2.png"}, { "frame" : 3, "file" : "level-3.png"}, { "frame" : 4, "file" : "level-4.png"}, { "frame" : 5, "file" : "level-5.png"}, { "frame" : 6, "file" : "level-6.png"}, { "frame" : 7, "file" : "level-7.png"}, { "frame" : 8, "file" : "level-8.png"}, { "frame" : 9, "file" : "level-9.png"}, { "frame" : 10,"file" : "level-10.png"} ] } vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/switchModeIcons.json000066400000000000000000000001471342332007200267370ustar00rootroot00000000000000{ "images" : [ { "frame" : 0, "file" : "SECSK32:69"}, { "frame" : 1, "file" : "SECSK32:28"} ] } vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/upgrade-normal.png000066400000000000000000000022671342332007200263720ustar00rootroot00000000000000PNG  IHDR$$hgAMA a oFFs5! pHYs X =tIME  :}&&PLTE                !!""## $ $ % % & & ' ( ( ) * * * + + , - - SJDTKDUKDVLEWLEWMEXMEXNEYNEZNE[OE\OF]PF^PF_QG`QGxrysysyszsztzt{t{t{t{t|u|u}uN*bKGDaLIDAT[r#D {%uW 5PU>X k$RK*ڄvb*ZJ >~\͆v@Bjy3&~kV9 ‡Μ,ǭD-mUS_}FaUGS9w6g70à&́ފ٬;V'~Q잠K#AFVWN닇*Jq|o!>b>]<wZe UlHM߷L.Q82L_. 4ZytaHj-Kv[s4`0 "OБrԐ% # J}@!G֜.dԮmc"). JִEW`a`1=BcT6*KLEx vˑE3EN)Di)uܵڧВs}moS`NHuIm^ L[0<=nVpk9ſ-%Oױ{kK\?йZ+SlvMQ^Tx+MB0cH! oYf 9*lCd ߗE0[t{}֣:vių9./yI}fԪkyHI ~h\{(Oһ P1ASU n$@fM׆Q>:g 6FlP '"5u!sޣ+6aIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/upgrade-pressed.png000066400000000000000000000021131342332007200265350ustar00rootroot00000000000000PNG  IHDR$$hgAMA a oFFs5! pHYs X =tIME  :}&PLTE            !! " # # $ % % % & & ' ( ( IARF>SF>TG?UG?pjdqkerkeskeskfslftlfulfvlfvmgwmgwng|bKGDPnLIDAT8Oɑ,E@Kfuφ}0DBj 03RpA?jfH*%8_:hi 1w'ן󸞒5#e-fQ$7R*a{š6S.|Yt 1^ ʺ15K"vhw{ޕ{(m8*9>9:9G=ZY=dU䪜f(XvT_f+NrţZ μSQȚ'\Ec2:ϦV~[JI(;#%HB#*N$I 0st!s%>jCgQE=)Y=.o2%)H29DǼLsk)lLЂ25[CU{hM ˤUr[P( (:;֜'!h|s׊X6mdKӏ.3pcTQN7]Zg$. SmP=N`W돴[:eHP,-6RE{O'r|LXjK e'2u_L2JXm]|,z^ꆖ=%wb@_(pN1z|vaV[|S]!6k"Y@ QehL졊BG{L-RVD)jGGٲY M#|*6 MZjIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/stackWindow/upgradeButton.json000066400000000000000000000002261342332007200264560ustar00rootroot00000000000000{ "basepath" : "stackWindow/", "images" : [ { "frame" : 0, "file" : "upgrade-normal.png"}, { "frame" : 1, "file" : "upgrade-pressed.png"} ] } vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/vcmi/000077500000000000000000000000001342332007200214015ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/vcmi/battleQueue/000077500000000000000000000000001342332007200236615ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/vcmi/battleQueue/defendBig.png000066400000000000000000000047021342332007200262410ustar00rootroot00000000000000PNG  IHDR"$bKGD pHYs  tIME .K:NiTXtCommentCreated with GIMPd.e &IDATXݘmpTdf7.!6ȢA@S;)tlQ;qFeюTVc8$%$YI`ܛf%{d""D?g~wsipjooo,8 k0~0 fSƆ5n뾊`:#34l6Ff4hʼnGL*Vӧ^ o&L?;O5E(͎^O͎og?HQTj'2%32;&/FWEs@Q8hvZa E9C,2ȼKYavƒ!"-=lC$Xr^{nTab`_rΞnңlYp:hq%K0*m(67"c[Vrt1#2FN9 1ƃk KA_ 1>&3“-= :Hn88a;$D‘n{|Y&aXo;K2fuyuifwF4r hGt*2J+ٱgJ@),IG &k׋ a-QCd3:#p;H7B(@ '2 , ȾD:ZY6"ZNZ,x_z^A?džPdH );nvuNft}ĆO-o&$H%#X,NYIOMz/D1TT868|X|tG;۷sC>ʕwqph` `xx\:$2IE 7`p +)dG(u8aVx0gN;8|(Wj-L֒_ȭ1=щ1"SXej/t(c TTzEYpcHb'ϩC|w?0UNwE \;rN3L;1F)C>ܒD ;a?ad3:vbb&>z}mGܱVmoca,*K.d[ 1f/|7x˽ͼ{ 8vvlsyg3jj$= 1Gzoew^ET{{+[ek`)pszܝ53qVI39f-M(zoE"N?K/9;9+GIENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/vcmi/battleQueue/statesBig.json000066400000000000000000000002051342332007200264760ustar00rootroot00000000000000{ "basepath": "vcmi/battleQueue/", "images" : [ { "frame" : 0, "file" : "defendBig"}, { "frame" : 1, "file" : "waitBig"} ] } vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/vcmi/battleQueue/statesSmall.json000066400000000000000000000002111342332007200270420ustar00rootroot00000000000000{ "basepath": "vcmi/battleQueue/", "images" : [ { "frame" : 0, "file" : "defendSmall"}, { "frame" : 1, "file" : "waitSmall"} ] } vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/vcmi/battleQueue/waitBig.png000066400000000000000000000032711342332007200257600ustar00rootroot00000000000000PNG  IHDR$f1qbKGD pHYs  tIME /iTXtCommentCreated with GIMPd.eIDATHmlS/;i}8>ȇYd8) mJ) F- VTcU+ZM Q2hhd $zn'48~88x!o׭KA8DrU5?4е?r}A:| $GCya#Fiq!'O`E10MQ)aL359JQqvfQqp ΍$2Y%׸wvcvU1? IB~?@+Ȱf!hK6&C_)W.q#4 1_\= Z^X!܈:7BOaj . }A47Ql)wvpR'[lKɦh3bԬ}SdĚsdXL3Ģvz(T A,``603r$k5+(1@bm UjhCddXq9&%"q:e֖%̓g*O|;]ll}Լ,7>@Oo/N~jCD@J|9:4kҏ U.M=!*5@ LJUYv._>?| 4GGLcۮt(-)XN5|MB$qcyEA(lmĦȢNKqlK@=x+Ⓝb& ENmk綋OM4ѡgL|H |QLLRIX3ig;g!MUOm='o*%2ʪMڭ)bwzE/ɭ)k,qcτIGC(63Y8}NOFfb[M!9d ~ud>Gh⾫g--} _0xT~ 0gb[B:}~JˊAD͚oN`0/D a= *پ[^gɽm/ovRcuQ瞮G7 u[SC QUP^^Noo/kkVE%dex|QRHæ}gԘMTK] #Fnʨ@Ǘwo0 ì )W(v&QD&%N#t~cΒp%@9v|;VK99|L3=N٣ްkWUXC4e'/ark.?kfq3%y<~-;f *5 =UdcuSJe:-G7ik;Nw'l}}@`޷|7mkŏRݽN (֡;cD:O1 ھvR j_Sm ^kv,r2t3*h p$Ln g3be;a w{k|"Y߀)͊s0[R_["p(Rovoщ(IENDB`vcmi-0.99+git20190113.f06c8a87/Mods/vcmi/Sprites/vcmi/battleQueue/waitSmall.png000066400000000000000000000013671342332007200263330ustar00rootroot00000000000000PNG  IHDR4%bKGD pHYs  tIME /4-biTXtCommentCreated with GIMPd.e[IDAT8˽S]HSq=cswswwΩMFjl-&+1"{(E IO)Bބ   s3Hm#WI=tϏ/Awo:))h%dAp>p灪Yjkͪ~C$]kTm#ESfF+@]j c~1I+މTkOE:Pr6KYSkp wMY93GYZUnU 2{l&2VD-tw K pu!l)Jo6h>fRR}. N2E!ފ;ϝt,[ I?#1|BOgK8j?HT~>ϙ/ɴ `Vѳ @)huD/!;{B|yeۭ7pHVyZujQs@!??8A8A[VjyږF ҟ7}=ccM}\ <_PropertySheetDisplayName>VCMI_global $(SolutionDir)\deps\libs;$(ProjectDir);$(LibraryPath) $(SolutionDir)\deps\include;$(SolutionDir)\include;$(ProjectDir);$(IncludePath) $(VCMI_Out)\ Console 4275 vcmi-0.99+git20190113.f06c8a87/VCMI_global_debug.props000066400000000000000000000006741342332007200214720ustar00rootroot00000000000000 MultiThreadedDebugDLL true vcmi-0.99+git20190113.f06c8a87/VCMI_global_release.props000066400000000000000000000016261342332007200220220ustar00rootroot00000000000000 MaxSpeed true Speed true MultiThreadedDLL true vcmi-0.99+git20190113.f06c8a87/VCMI_global_user.props000066400000000000000000000012301342332007200213470ustar00rootroot00000000000000 D:\Program files co nie\Qt\5.9.1\msvc2015\ D:\VCMI\vcmi_devversion $(QTDIR) $(VCMI_Out) true vcmi-0.99+git20190113.f06c8a87/Version.cpp.in000066400000000000000000000004741342332007200177150ustar00rootroot00000000000000/* * Version.cpp.in, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "Version.h" namespace GameConstants { const char GIT_SHA1[] = "@GIT_SHA1@"; } vcmi-0.99+git20190113.f06c8a87/Version.h000066400000000000000000000001101342332007200167400ustar00rootroot00000000000000#pragma once namespace GameConstants { extern const char GIT_SHA1[]; } vcmi-0.99+git20190113.f06c8a87/client/000077500000000000000000000000001342332007200164305ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/client/CBitmapHandler.cpp000066400000000000000000000110271342332007200217520ustar00rootroot00000000000000/* * CBitmapHandler.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "../lib/filesystem/Filesystem.h" #include "SDL.h" #include "SDL_image.h" #include "CBitmapHandler.h" #include "gui/SDL_Extensions.h" #include "../lib/vcmi_endian.h" namespace BitmapHandler { SDL_Surface * loadH3PCX(ui8 * data, size_t size); SDL_Surface * loadBitmapFromDir(std::string path, std::string fname, bool setKey=true); } bool isPCX(const ui8 *header)//check whether file can be PCX according to header { ui32 fSize = read_le_u32(header + 0); ui32 width = read_le_u32(header + 4); ui32 height = read_le_u32(header + 8); return fSize == width*height || fSize == width*height*3; } enum Epcxformat { PCX8B, PCX24B }; SDL_Surface * BitmapHandler::loadH3PCX(ui8 * pcx, size_t size) { SDL_Surface * ret; Epcxformat format; int it=0; ui32 fSize = read_le_u32(pcx + it); it+=4; ui32 width = read_le_u32(pcx + it); it+=4; ui32 height = read_le_u32(pcx + it); it+=4; if (fSize==width*height*3) format=PCX24B; else if (fSize==width*height) format=PCX8B; else return nullptr; if (format==PCX8B) { ret = SDL_CreateRGBSurface(0, width, height, 8, 0, 0, 0, 0); it = 0xC; for (int i=0; ipixels + ret->pitch * i, pcx + it, width); it+= width; } //palette - last 256*3 bytes it = size-256*3; for (int i=0;i<256;i++) { SDL_Color tp; tp.r = pcx[it++]; tp.g = pcx[it++]; tp.b = pcx[it++]; tp.a = SDL_ALPHA_OPAQUE; ret->format->palette->colors[i] = tp; } } else { #if (SDL_BYTEORDER == SDL_BIG_ENDIAN) int bmask = 0xff0000; int gmask = 0x00ff00; int rmask = 0x0000ff; #else int bmask = 0x0000ff; int gmask = 0x00ff00; int rmask = 0xff0000; #endif ret = SDL_CreateRGBSurface(0, width, height, 24, rmask, gmask, bmask, 0); //it == 0xC; for (int i=0; ipixels + ret->pitch * i, pcx + it, width*3); it+= width*3; } } return ret; } SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fname, bool setKey) { if(!fname.size()) { logGlobal->warn("Call to loadBitmap with void fname!"); return nullptr; } if (!CResourceHandler::get()->existsResource(ResourceID(path + fname, EResType::IMAGE))) { return nullptr; } SDL_Surface * ret=nullptr; auto readFile = CResourceHandler::get()->load(ResourceID(path + fname, EResType::IMAGE))->readAll(); if (isPCX(readFile.first.get())) {//H3-style PCX ret = loadH3PCX(readFile.first.get(), readFile.second); if (ret) { if(ret->format->BytesPerPixel == 1 && setKey) { CSDL_Ext::setColorKey(ret,ret->format->palette->colors[0]); } } else { logGlobal->error("Failed to open %s as H3 PCX!", fname); return nullptr; } } else { //loading via SDL_Image ret = IMG_Load_RW( //create SDL_RW with our data (will be deleted by SDL) SDL_RWFromConstMem((void*)readFile.first.get(), readFile.second), 1); // mark it for auto-deleting if (ret) { if (ret->format->palette) { //set correct value for alpha\unused channel for (int i=0; i < ret->format->palette->ncolors; i++) ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE; } } else { logGlobal->error("Failed to open %s via SDL_Image", fname); logGlobal->error("Reason: %s", IMG_GetError()); return nullptr; } } // When modifying anything here please check two use cases: // 1) Vampire mansion in Necropolis (not 1st color is transparent) // 2) Battle background when fighting on grass/dirt, topmost sky part (NO transparent color) // 3) New objects that may use 24-bit images for icons (e.g. witchking arts) if (ret->format->palette) { CSDL_Ext::setDefaultColorKeyPresize(ret); } else if (ret->format->Amask) { SDL_SetSurfaceBlendMode(ret, SDL_BLENDMODE_BLEND); } else // always set { CSDL_Ext::setDefaultColorKey(ret); } return ret; } SDL_Surface * BitmapHandler::loadBitmap(std::string fname, bool setKey) { SDL_Surface * bitmap = nullptr; if (!(bitmap = loadBitmapFromDir("DATA/", fname, setKey)) && !(bitmap = loadBitmapFromDir("SPRITES/", fname, setKey))) { logGlobal->error("Error: Failed to find file %s", fname); } return bitmap; } vcmi-0.99+git20190113.f06c8a87/client/CBitmapHandler.h000066400000000000000000000006331342332007200214200ustar00rootroot00000000000000/* * CBitmapHandler.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once struct SDL_Surface; namespace BitmapHandler { //Load file from /DATA or /SPRITES SDL_Surface * loadBitmap(std::string fname, bool setKey=true); } vcmi-0.99+git20190113.f06c8a87/client/CGameInfo.cpp000066400000000000000000000015061342332007200207260ustar00rootroot00000000000000/* * CGameInfo.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CGameInfo.h" #include "../lib/CSkillHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/VCMI_Lib.h" const CGameInfo * CGI; CClientState * CCS = nullptr; CServerHandler * CSH; CGameInfo::CGameInfo() { generaltexth = nullptr; mh = nullptr; townh = nullptr; } void CGameInfo::setFromLib() { modh = VLC->modh; generaltexth = VLC->generaltexth; arth = VLC->arth; creh = VLC->creh; townh = VLC->townh; heroh = VLC->heroh; objh = VLC->objh; spellh = VLC->spellh; skillh = VLC->skillh; objtypeh = VLC->objtypeh; } vcmi-0.99+git20190113.f06c8a87/client/CGameInfo.h000066400000000000000000000031751342332007200203770ustar00rootroot00000000000000/* * CGameInfo.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../lib/ConstTransitivePtr.h" class CModHandler; class CMapHandler; class CArtHandler; class CHeroHandler; class CCreatureHandler; class CSpellHandler; class CSkillHandler; class CBuildingHandler; class CObjectHandler; class CSoundHandler; class CMusicHandler; class CObjectClassesHandler; class CTownHandler; class CGeneralTextHandler; class CConsoleHandler; class CCursorHandler; class CGameState; class IMainVideoPlayer; class CServerHandler; class CMap; //a class for non-mechanical client GUI classes class CClientState { public: CSoundHandler * soundh; CMusicHandler * musich; CConsoleHandler * consoleh; CCursorHandler * curh; IMainVideoPlayer * videoh; }; extern CClientState * CCS; /// CGameInfo class /// for allowing different functions for accessing game informations class CGameInfo { public: ConstTransitivePtr modh; //public? ConstTransitivePtr arth; ConstTransitivePtr heroh; ConstTransitivePtr creh; ConstTransitivePtr spellh; ConstTransitivePtr skillh; ConstTransitivePtr objh; ConstTransitivePtr objtypeh; CGeneralTextHandler * generaltexth; CMapHandler * mh; CTownHandler * townh; void setFromLib(); friend class CClient; CGameInfo(); }; extern const CGameInfo* CGI; vcmi-0.99+git20190113.f06c8a87/client/CMT.cpp000066400000000000000000001173111342332007200175630ustar00rootroot00000000000000/* * CMT.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ // CMT.cpp : Defines the entry point for the console application. // #include "StdInc.h" #include #include #include "gui/SDL_Extensions.h" #include "CGameInfo.h" #include "mapHandler.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/FileStream.h" #include "mainmenu/CMainMenu.h" #include "lobby/CSelectionBase.h" #include "windows/CCastleInterface.h" #include "../lib/CConsoleHandler.h" #include "gui/CCursorHandler.h" #include "../lib/CGameState.h" #include "../CCallback.h" #include "CPlayerInterface.h" #include "windows/CAdvmapInterface.h" #include "../lib/CBuildingHandler.h" #include "CVideoHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/spells/CSpellHandler.h" #include "CMusicHandler.h" #include "../lib/CGeneralTextHandler.h" #include "Graphics.h" #include "Client.h" #include "../lib/CConfigHandler.h" #include "../lib/serializer/BinaryDeserializer.h" #include "../lib/serializer/BinarySerializer.h" #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" #include "../lib/NetPacks.h" #include "CMessage.h" #include "../lib/CModHandler.h" #include "../lib/CTownHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CScriptingModule.h" #include "../lib/GameConstants.h" #include "gui/CGuiHandler.h" #include "../lib/logging/CBasicLogConfigurator.h" #include "../lib/StringConstants.h" #include "../lib/CPlayerState.h" #include "gui/CAnimation.h" #include "../lib/serializer/Connection.h" #include "CServerHandler.h" #include #include "mainmenu/CPrologEpilogVideo.h" #include #ifdef VCMI_WINDOWS #include "SDL_syswm.h" #endif #ifdef VCMI_ANDROID #include "lib/CAndroidVMHelper.h" #endif #include "../lib/UnlockGuard.h" #include "CMT.h" #if __MINGW32__ #undef main #endif namespace po = boost::program_options; namespace po_style = boost::program_options::command_line_style; namespace bfs = boost::filesystem; std::string NAME_AFFIX = "client"; std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name CGuiHandler GH; int preferredDriverIndex = -1; SDL_Window * mainWindow = nullptr; SDL_Renderer * mainRenderer = nullptr; SDL_Texture * screenTexture = nullptr; extern boost::thread_specific_ptr inGuiThread; SDL_Surface *screen = nullptr, //main screen surface *screen2 = nullptr, //and hlp surface (used to store not-active interfaces layer) *screenBuf = screen; //points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed std::queue events; boost::mutex eventsM; static po::variables_map vm; //static bool setResolution = false; //set by event handling thread after resolution is adjusted static bool ermInteractiveMode = false; //structurize when time is right void processCommand(const std::string &message); static void setScreenRes(int w, int h, int bpp, bool fullscreen, int displayIndex, bool resetVideo=true); void playIntro(); static void mainLoop(); #ifndef VCMI_WINDOWS #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #endif void init() { CStopWatch tmh; loadDLLClasses(); const_cast(CGI)->setFromLib(); logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff()); } static void prog_version() { printf("%s\n", GameConstants::VCMI_VERSION.c_str()); std::cout << VCMIDirs::get().genHelpString(); } static void prog_help(const po::options_description &opts) { auto time = std::time(0); printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); printf("This is free software; see the source for copying conditions. There is NO\n"); printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); printf("\n"); std::cout << opts; } static void SDLLogCallback(void* userdata, int category, SDL_LogPriority priority, const char* message) { //todo: convert SDL log priority to vcmi log priority //todo: make separate log domain for SDL logGlobal->debug("SDL(category %d; priority %d) %s", category, priority, message); } #if defined(VCMI_WINDOWS) && !defined(__GNUC__) && defined(VCMI_WITH_DEBUG_CONSOLE) int wmain(int argc, wchar_t* argv[]) #elif defined(VCMI_APPLE) || defined(VCMI_ANDROID) int SDL_main(int argc, char *argv[]) #else int main(int argc, char * argv[]) #endif { #ifdef VCMI_ANDROID // boost will crash without this setenv("LANG", "C", 1); #endif #ifndef VCMI_ANDROID // Correct working dir executable folder (not bundle folder) so we can use executable relative paths boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); #endif std::cout << "Starting... " << std::endl; po::options_description opts("Allowed options"); opts.add_options() ("help,h", "display help and exit") ("version,v", "display version information and exit") ("disable-shm", "force disable shared memory usage") ("enable-shm-uuid", "use UUID for shared memory identifier") ("testmap", po::value(), "") ("testsave", po::value(), "") ("spectate,s", "enable spectator interface for AI-only games") ("spectate-ignore-hero", "wont follow heroes on adventure map") ("spectate-hero-speed", po::value(), "hero movement speed on adventure map") ("spectate-battle-speed", po::value(), "battle animation speed for spectator") ("spectate-skip-battle", "skip battles in spectator view") ("spectate-skip-battle-result", "skip battle result window") ("onlyAI", "allow to run without human player, all players will be default AI") ("headless", "runs without GUI, implies --onlyAI") ("ai", po::value>(), "AI to be used for the player, can be specified several times for the consecutive players") ("oneGoodAI", "puts one default AI and the rest will be EmptyAI") ("autoSkip", "automatically skip turns in GUI") ("disable-video", "disable video player") ("nointro,i", "skips intro movies") ("donotstartserver,d","do not attempt to start server and just connect to it instead server") ("serverport", po::value(), "override port specified in config file") ("saveprefix", po::value(), "prefix for auto save files") ("savefrequency", po::value(), "limit auto save creation to each N days"); if(argc > 1) { try { po::store(po::parse_command_line(argc, argv, opts, po_style::unix_style|po_style::case_insensitive), vm); } catch(std::exception &e) { std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; } } po::notify(vm); if(vm.count("help")) { prog_help(opts); return 0; } if(vm.count("version")) { prog_version(); return 0; } // Init old logging system and new (temporary) logging system CStopWatch total, pomtime; std::cout.flags(std::ios::unitbuf); console = new CConsoleHandler(); *console->cb = processCommand; console->start(); const bfs::path logPath = VCMIDirs::get().userCachePath() / "VCMI_Client_log.txt"; CBasicLogConfigurator logConfig(logPath, console); logConfig.configureDefault(); logGlobal->info(NAME); logGlobal->info("Creating console and configuring logger: %d ms", pomtime.getDiff()); logGlobal->info("The log file will be saved to %s", logPath); // Init filesystem and settings preinitDLL(::console); settings.init(); Settings session = settings.write["session"]; auto setSettingBool = [](std::string key, std::string arg) { Settings s = settings.write(vstd::split(key, "/")); if(::vm.count(arg)) s->Bool() = true; else if(s->isNull()) s->Bool() = false; }; auto setSettingInteger = [](std::string key, std::string arg, si64 defaultValue) { Settings s = settings.write(vstd::split(key, "/")); if(::vm.count(arg)) s->Integer() = ::vm[arg].as(); else if(s->isNull()) s->Integer() = defaultValue; }; auto setSettingString = [](std::string key, std::string arg, std::string defaultValue) { Settings s = settings.write(vstd::split(key, "/")); if(::vm.count(arg)) s->String() = ::vm[arg].as(); else if(s->isNull()) s->String() = defaultValue; }; setSettingBool("session/onlyai", "onlyAI"); if(vm.count("headless")) { session["headless"].Bool() = true; session["onlyai"].Bool() = true; } else if(vm.count("spectate")) { session["spectate"].Bool() = true; session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero"); session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle"); session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result"); if(vm.count("spectate-hero-speed")) session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as(); if(vm.count("spectate-battle-speed")) session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as(); } // Server settings setSettingBool("session/donotstartserver", "donotstartserver"); // Shared memory options setSettingBool("session/disable-shm", "disable-shm"); setSettingBool("session/enable-shm-uuid", "enable-shm-uuid"); // Init special testing settings setSettingInteger("session/serverport", "serverport", 0); setSettingString("session/saveprefix", "saveprefix", ""); setSettingInteger("general/saveFrequency", "savefrequency", 1); // Initialize logging based on settings logConfig.configure(); logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); // Some basic data validation to produce better error messages in cases of incorrect install auto testFile = [](std::string filename, std::string message) -> bool { if (CResourceHandler::get()->existsResource(ResourceID(filename))) return true; logGlobal->error("Error: %s was not found!", message); return false; }; if (!testFile("DATA/HELP.TXT", "Heroes III data") || !testFile("MODS/VCMI/MOD.JSON", "VCMI data")) { exit(1); // These are unrecoverable errors } // these two are optional + some installs have them on CD and not in data directory testFile("VIDEO/GOOD1A.SMK", "campaign movies"); testFile("SOUNDS/G1A.WAV", "campaign music"); //technically not a music but voiced intro sounds conf.init(); logGlobal->info("Loading settings: %d ms", pomtime.getDiff()); srand ( time(nullptr) ); const JsonNode& video = settings["video"]; const JsonNode& res = video["screenRes"]; //something is really wrong... if (res["width"].Float() < 100 || res["height"].Float() < 100) { logGlobal->error("Fatal error: failed to load settings!"); logGlobal->error("Possible reasons:"); logGlobal->error("\tCorrupted local configuration file at %s/settings.json", VCMIDirs::get().userConfigPath()); logGlobal->error("\tMissing or corrupted global configuration file at %s/schemas/settings.json", VCMIDirs::get().userConfigPath()); logGlobal->error("VCMI will now exit..."); exit(EXIT_FAILURE); } if(!settings["session"]["headless"].Bool()) { if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_AUDIO|SDL_INIT_NOPARACHUTE)) { logGlobal->error("Something was wrong: %s", SDL_GetError()); exit(-1); } #ifdef VCMI_ANDROID // manually setting egl pixel format, as a possible solution for sdl2<->android problem // https://bugzilla.libsdl.org/show_bug.cgi?id=2291 SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0); #endif // VCMI_ANDROID GH.mainFPSmng->init(); //(!)init here AFTER SDL_Init() while using SDL for FPS management SDL_LogSetOutputFunction(&SDLLogCallback, nullptr); int driversCount = SDL_GetNumRenderDrivers(); std::string preferredDriverName = video["driver"].String(); logGlobal->info("Found %d render drivers", driversCount); for(int it = 0; it < driversCount; it++) { SDL_RendererInfo info; SDL_GetRenderDriverInfo(it,&info); std::string driverName(info.name); if(!preferredDriverName.empty() && driverName == preferredDriverName) { preferredDriverIndex = it; logGlobal->info("\t%s (active)", driverName); } else logGlobal->info("\t%s", driverName); } config::CConfigHandler::GuiOptionsMap::key_type resPair(res["width"].Float(), res["height"].Float()); if (conf.guiOptions.count(resPair) == 0) { // selected resolution was not found - complain & fallback to something that we do have. logGlobal->error("Selected resolution %dx%d was not found!", resPair.first, resPair.second); if (conf.guiOptions.empty()) { logGlobal->error("Unable to continue - no valid resolutions found! Please reinstall VCMI to fix this"); exit(1); } else { Settings newRes = settings.write["video"]["screenRes"]; newRes["width"].Float() = conf.guiOptions.begin()->first.first; newRes["height"].Float() = conf.guiOptions.begin()->first.second; conf.SetResolution(newRes["width"].Float(), newRes["height"].Float()); logGlobal->error("Falling back to %dx%d", newRes["width"].Integer(), newRes["height"].Integer()); } } setScreenRes(res["width"].Float(), res["height"].Float(), video["bitsPerPixel"].Float(), video["fullscreen"].Bool(), video["displayIndex"].Float()); logGlobal->info("\tInitializing screen: %d ms", pomtime.getDiff()); } CCS = new CClientState(); CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.) CSH = new CServerHandler(); // Initialize video #ifdef DISABLE_VIDEO CCS->videoh = new CEmptyVideoPlayer(); #else if (!settings["session"]["headless"].Bool() && !vm.count("disable-video")) CCS->videoh = new CVideoPlayer(); else CCS->videoh = new CEmptyVideoPlayer(); #endif logGlobal->info("\tInitializing video: %d ms", pomtime.getDiff()); if(!settings["session"]["headless"].Bool()) { //initializing audio CCS->soundh = new CSoundHandler(); CCS->soundh->init(); CCS->soundh->setVolume(settings["general"]["sound"].Float()); CCS->musich = new CMusicHandler(); CCS->musich->init(); CCS->musich->setVolume(settings["general"]["music"].Float()); logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff()); } #ifdef __APPLE__ // Ctrl+click should be treated as a right click on Mac OS X SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1"); #endif #ifndef VCMI_NO_THREADED_LOAD //we can properly play intro only in the main thread, so we have to move loading to the separate thread boost::thread loading(init); #else init(); #endif if(!settings["session"]["headless"].Bool()) { if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool()) playIntro(); SDL_SetRenderDrawColor(mainRenderer, 0, 0, 0, 255); SDL_RenderClear(mainRenderer); SDL_RenderPresent(mainRenderer); } #ifndef VCMI_NO_THREADED_LOAD #ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds { CAndroidVMHelper vmHelper; vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "showProgress"); #endif // ANDROID loading.join(); #ifdef VCMI_ANDROID vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "hideProgress"); } #endif // ANDROID #endif // THREADED if(!settings["session"]["headless"].Bool()) { pomtime.getDiff(); CCS->curh = new CCursorHandler(); graphics = new Graphics(); // should be before curh->init() CCS->curh->initCursor(); logGlobal->info("Screen handler: %d ms", pomtime.getDiff()); pomtime.getDiff(); graphics->load();//must be after Content loading but should be in main thread logGlobal->info("Main graphics: %d ms", pomtime.getDiff()); CMessage::init(); logGlobal->info("Message handler: %d ms", pomtime.getDiff()); CCS->curh->show(); } logGlobal->info("Initialization of VCMI (together): %d ms", total.getDiff()); session["autoSkip"].Bool() = vm.count("autoSkip"); session["oneGoodAI"].Bool() = vm.count("oneGoodAI"); session["aiSolo"].Bool() = false; if(vm.count("testmap")) { session["testmap"].String() = vm["testmap"].as(); session["onlyai"].Bool() = true; boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false); } else if(vm.count("testsave")) { session["testsave"].String() = vm["testsave"].as(); session["onlyai"].Bool() = true; boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true); } else { GH.curInt = CMainMenu::create().get(); } if(!settings["session"]["headless"].Bool()) { mainLoop(); } else { while(true) boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); } return 0; } void printInfoAboutIntObject(const CIntObject *obj, int level) { std::stringstream sbuffer; sbuffer << std::string(level, '\t'); sbuffer << typeid(*obj).name() << " *** "; if (obj->active) { #define PRINT(check, text) if (obj->active & CIntObject::check) sbuffer << text PRINT(LCLICK, 'L'); PRINT(RCLICK, 'R'); PRINT(HOVER, 'H'); PRINT(MOVE, 'M'); PRINT(KEYBOARD, 'K'); PRINT(TIME, 'T'); PRINT(GENERAL, 'A'); PRINT(WHEEL, 'W'); PRINT(DOUBLECLICK, 'D'); #undef PRINT } else sbuffer << "inactive"; sbuffer << " at " << obj->pos.x <<"x"<< obj->pos.y; sbuffer << " (" << obj->pos.w <<"x"<< obj->pos.h << ")"; logGlobal->info(sbuffer.str()); for(const CIntObject *child : obj->children) printInfoAboutIntObject(child, level+1); } void removeGUI() { // CClient::endGame GH.curInt = nullptr; if(GH.topInt()) GH.topInt()->deactivate(); GH.listInt.clear(); GH.objsToBlit.clear(); GH.statusbar = nullptr; logGlobal->info("Removed GUI."); LOCPLINT = nullptr; } void processCommand(const std::string &message) { std::istringstream readed; readed.str(message); std::string cn; //command name readed >> cn; // Check mantis issue 2292 for details // if(LOCPLINT && LOCPLINT->cingconsole) // LOCPLINT->cingconsole->print(message); if(ermInteractiveMode) { if(cn == "exit") { ermInteractiveMode = false; return; } else { if(CSH->client && CSH->client->erm) CSH->client->erm->executeUserCommand(message); std::cout << "erm>"; } } else if(message==std::string("die, fool")) { exit(EXIT_SUCCESS); } else if(cn == "erm") { ermInteractiveMode = true; std::cout << "erm>"; } else if(cn==std::string("activate")) { int what; readed >> what; switch (what) { case 0: GH.topInt()->activate(); break; case 1: adventureInt->activate(); break; case 2: LOCPLINT->castleInt->activate(); break; } } else if(cn=="redraw") { GH.totalRedraw(); } else if(cn=="screen") { std::cout << "Screenbuf points to "; if(screenBuf == screen) logGlobal->error("screen"); else if(screenBuf == screen2) logGlobal->error("screen2"); else logGlobal->error("?!?"); SDL_SaveBMP(screen, "Screen_c.bmp"); SDL_SaveBMP(screen2, "Screen2_c.bmp"); } else if(cn=="save") { if(!CSH->client) { std::cout << "Game in not active"; return; } std::string fname; readed >> fname; CSH->client->save(fname); } // else if(cn=="load") // { // // TODO: this code should end the running game and manage to call startGame instead // std::string fname; // readed >> fname; // CSH->client->loadGame(fname); // } else if(message=="convert txt") { std::cout << "Command accepted.\t"; const bfs::path outPath = VCMIDirs::get().userCachePath() / "extracted"; bfs::create_directories(outPath); auto extractVector = [=](const std::vector & source, const std::string & name) { JsonNode data(JsonNode::JsonType::DATA_VECTOR); size_t index = 0; for(auto & line : source) { JsonNode lineNode(JsonNode::JsonType::DATA_STRUCT); lineNode["text"].String() = line; lineNode["index"].Integer() = index++; data.Vector().push_back(lineNode); } const bfs::path filePath = outPath / (name + ".json"); bfs::ofstream file(filePath); file << data.toJson(); }; extractVector(VLC->generaltexth->allTexts, "generalTexts"); extractVector(VLC->generaltexth->jktexts, "jkTexts"); extractVector(VLC->generaltexth->arraytxt, "arrayTexts"); std::cout << "\rExtracting done :)\n"; std::cout << " Extracted files can be found in " << outPath << " directory\n"; } else if(message=="get config") { std::cout << "Command accepted.\t"; const bfs::path outPath = VCMIDirs::get().userCachePath() / "extracted" / "configuration"; bfs::create_directories(outPath); const std::vector contentNames = {"heroClasses", "artifacts", "creatures", "factions", "objects", "heroes", "spells", "skills"}; for(auto contentName : contentNames) { auto & content = VLC->modh->content[contentName]; auto contentOutPath = outPath / contentName; bfs::create_directories(contentOutPath); for(auto & iter : content.modData) { const JsonNode & modData = iter.second.modData; for(auto & nameAndObject : modData.Struct()) { const JsonNode & object = nameAndObject.second; std::string name = CModHandler::normalizeIdentifier(object.meta, "core", nameAndObject.first); boost::algorithm::replace_all(name,":","_"); const bfs::path filePath = contentOutPath / (name + ".json"); bfs::ofstream file(filePath); file << object.toJson(); } } } std::cout << "\rExtracting done :)\n"; std::cout << " Extracted files can be found in " << outPath << " directory\n"; } else if(message=="get txt") { std::cout << "Command accepted.\t"; const bfs::path outPath = VCMIDirs::get().userCachePath() / "extracted"; auto list = CResourceHandler::get()->getFilteredFiles([](const ResourceID & ident) { return ident.getType() == EResType::TEXT && boost::algorithm::starts_with(ident.getName(), "DATA/"); }); for (auto & filename : list) { const bfs::path filePath = outPath / (filename.getName() + ".TXT"); bfs::create_directories(filePath.parent_path()); bfs::ofstream file(filePath); auto text = CResourceHandler::get()->load(filename)->readAll(); file.write((char*)text.first.get(), text.second); } std::cout << "\rExtracting done :)\n"; std::cout << " Extracted files can be found in " << outPath << " directory\n"; } else if(cn=="crash") { int *ptr = nullptr; *ptr = 666; //disaster! } else if(cn == "mp" && adventureInt) { if(const CGHeroInstance *h = dynamic_cast(adventureInt->selection)) std::cout << h->movement << "; max: " << h->maxMovePoints(true) << "/" << h->maxMovePoints(false) << std::endl; } else if(cn == "bonuses") { bool jsonFormat = (message == "bonuses json"); auto format = [jsonFormat](const BonusList & b) -> std::string { if(jsonFormat) return b.toJsonNode().toJson(true); std::ostringstream ss; ss << b; return ss.str(); }; std::cout << "Bonuses of " << adventureInt->selection->getObjectName() << std::endl << format(adventureInt->selection->getBonusList()) << std::endl; std::cout << "\nInherited bonuses:\n"; TCNodes parents; adventureInt->selection->getParents(parents); for(const CBonusSystemNode *parent : parents) { std::cout << "\nBonuses from " << typeid(*parent).name() << std::endl << format(*parent->getAllBonuses(Selector::all, Selector::all)) << std::endl; } } else if(cn == "not dialog") { LOCPLINT->showingDialog->setn(false); } else if(cn == "gui") { for(auto & child : GH.listInt) { const auto childPtr = child.get(); if(const CIntObject * obj = dynamic_cast(childPtr)) printInfoAboutIntObject(obj, 0); else std::cout << typeid(childPtr).name() << std::endl; } } else if(cn=="tell") { std::string what; int id1, id2; readed >> what >> id1 >> id2; if(what == "hs") { for(const CGHeroInstance *h : LOCPLINT->cb->getHeroesInfo()) if(h->type->ID.getNum() == id1) if(const CArtifactInstance *a = h->getArt(ArtifactPosition(id2))) std::cout << a->nodeName(); } } else if (cn == "set") { std::string what, value; readed >> what; Settings conf = settings.write["session"][what]; readed >> value; if (value == "on") { conf->Bool() = true; logGlobal->info("Option %s enabled!", what); } else if (value == "off") { conf->Bool() = false; logGlobal->info("Option %s disabled!", what); } } else if(cn == "unlock") { std::string mxname; readed >> mxname; if(mxname == "pim" && LOCPLINT) LOCPLINT->pim->unlock(); } else if(cn == "def2bmp") { std::string URI; readed >> URI; std::unique_ptr anim = make_unique(URI); anim->preload(); anim->exportBitmaps(VCMIDirs::get().userCachePath() / "extracted"); } else if(cn == "extract") { std::string URI; readed >> URI; if (CResourceHandler::get()->existsResource(ResourceID(URI))) { const bfs::path outPath = VCMIDirs::get().userCachePath() / "extracted" / URI; auto data = CResourceHandler::get()->load(ResourceID(URI))->readAll(); bfs::create_directories(outPath.parent_path()); bfs::ofstream outFile(outPath, bfs::ofstream::binary); outFile.write((char*)data.first.get(), data.second); } else logGlobal->error("File not found!"); } else if(cn == "setBattleAI") { std::string fname; readed >> fname; std::cout << "Will try loading that AI to see if it is correct name...\n"; try { if(auto ai = CDynLibHandler::getNewBattleAI(fname)) //test that given AI is indeed available... heavy but it is easy to make a typo and break the game { Settings neutralAI = settings.write["server"]["neutralAI"]; neutralAI->String() = fname; std::cout << "Setting changed, from now the battle ai will be " << fname << "!\n"; } } catch(std::exception &e) { logGlobal->warn("Failed opening %s: %s", fname, e.what()); logGlobal->warn("Setting not changes, AI not found or invalid!"); } } auto giveTurn = [&](PlayerColor player) { YourTurn yt; yt.player = player; yt.daysWithoutCastle = CSH->client->getPlayer(player)->daysWithoutCastle; yt.applyCl(CSH->client); }; Settings session = settings.write["session"]; if(cn == "autoskip") { session["autoSkip"].Bool() = !session["autoSkip"].Bool(); } else if(cn == "gosolo") { boost::unique_lock un(*CPlayerInterface::pim); if(!CSH->client) { std::cout << "Game in not active"; return; } PlayerColor color; if(session["aiSolo"].Bool()) { for(auto & elem : CSH->client->gameState()->players) { if(elem.second.human) CSH->client->installNewPlayerInterface(std::make_shared(elem.first), elem.first); } } else { color = LOCPLINT->playerID; removeGUI(); for(auto & elem : CSH->client->gameState()->players) { if(elem.second.human) { auto AiToGive = CSH->client->aiNameForPlayer(*CSH->client->getPlayerSettings(elem.first), false); logNetwork->info("Player %s will be lead by %s", elem.first, AiToGive); CSH->client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), elem.first); } } GH.totalRedraw(); giveTurn(color); } session["aiSolo"].Bool() = !session["aiSolo"].Bool(); } else if(cn == "controlai") { std::string colorName; readed >> colorName; boost::to_lower(colorName); boost::unique_lock un(*CPlayerInterface::pim); if(!CSH->client) { std::cout << "Game in not active"; return; } PlayerColor color; if(LOCPLINT) color = LOCPLINT->playerID; for(auto & elem : CSH->client->gameState()->players) { if(elem.second.human || (colorName.length() && elem.first.getNum() != vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, colorName))) { continue; } removeGUI(); CSH->client->installNewPlayerInterface(std::make_shared(elem.first), elem.first); } GH.totalRedraw(); if(color != PlayerColor::NEUTRAL) giveTurn(color); } // Check mantis issue 2292 for details /* else if(client && client->serv && client->serv->connected && LOCPLINT) //send to server { boost::unique_lock un(*CPlayerInterface::pim); LOCPLINT->cb->sendMessage(message); }*/ } //plays intro, ends when intro is over or button has been pressed (handles events) void playIntro() { if(CCS->videoh->openAndPlayVideo("3DOLOGO.SMK", 0, 1, true, true)) { CCS->videoh->openAndPlayVideo("AZVS.SMK", 0, 1, true, true); } } static bool checkVideoMode(int monitorIndex, int w, int h) { //we only check that our desired window size fits on screen SDL_DisplayMode mode; if (0 != SDL_GetDesktopDisplayMode(monitorIndex, &mode)) { logGlobal->error("SDL_GetDesktopDisplayMode failed"); logGlobal->error(SDL_GetError()); return false; } logGlobal->info("Check display mode: requested %d x %d; available up to %d x %d ", w, h, mode.w, mode.h); if (!mode.w || !mode.h || (w <= mode.w && h <= mode.h)) { return true; } return false; } static void cleanupRenderer() { screenBuf = nullptr; //it`s a link - just nullify if(nullptr != screen2) { SDL_FreeSurface(screen2); screen2 = nullptr; } if(nullptr != screen) { SDL_FreeSurface(screen); screen = nullptr; } if(nullptr != screenTexture) { SDL_DestroyTexture(screenTexture); screenTexture = nullptr; } } static bool recreateWindow(int w, int h, int bpp, bool fullscreen, int displayIndex) { // VCMI will only work with 2 or 4 bytes per pixel vstd::amax(bpp, 16); vstd::amin(bpp, 32); if(bpp>16) bpp = 32; if(displayIndex < 0) { if (mainWindow != nullptr) displayIndex = SDL_GetWindowDisplayIndex(mainWindow); if (displayIndex < 0) displayIndex = 0; } if(!checkVideoMode(displayIndex, w, h)) { logGlobal->error("Error: SDL says that %dx%d resolution is not available!", w, h); return false; } bool bufOnScreen = (screenBuf == screen); bool realFullscreen = settings["video"]["realFullscreen"].Bool(); cleanupRenderer(); if(nullptr == mainWindow) { #ifdef VCMI_ANDROID mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex),SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), 0, 0, SDL_WINDOW_FULLSCREEN); #else if(fullscreen) { if(realFullscreen) mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), w, h, SDL_WINDOW_FULLSCREEN); else //in windowed full-screen mode use desktop resolution mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex),SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), 0, 0, SDL_WINDOW_FULLSCREEN_DESKTOP); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); } else { mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex),SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex), w, h, 0); } #endif if(nullptr == mainWindow) { throw std::runtime_error("Unable to create window\n"); } //create first available renderer if preferred not set. Use no flags, so HW accelerated will be preferred but SW renderer also will possible mainRenderer = SDL_CreateRenderer(mainWindow,preferredDriverIndex,0); if(nullptr == mainRenderer) { throw std::runtime_error("Unable to create renderer\n"); } SDL_RendererInfo info; SDL_GetRendererInfo(mainRenderer, &info); logGlobal->info("Created renderer %s", info.name); } else { #ifndef VCMI_ANDROID if(fullscreen) { if(realFullscreen) { SDL_SetWindowFullscreen(mainWindow, SDL_WINDOW_FULLSCREEN); SDL_DisplayMode mode; SDL_GetDesktopDisplayMode(displayIndex, &mode); mode.w = w; mode.h = h; SDL_SetWindowDisplayMode(mainWindow, &mode); } else { SDL_SetWindowFullscreen(mainWindow, SDL_WINDOW_FULLSCREEN_DESKTOP); } SDL_SetWindowPosition(mainWindow, SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex)); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); } else { SDL_SetWindowFullscreen(mainWindow, 0); SDL_SetWindowSize(mainWindow, w, h); SDL_SetWindowPosition(mainWindow, SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex), SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex)); } #endif } if(!(fullscreen && realFullscreen)) { SDL_RenderSetLogicalSize(mainRenderer, w, h); //following line is bugged not only on android, do not re-enable without checking //#ifndef VCMI_ANDROID // // on android this stretches the game to fit the screen, not preserving aspect and apparently this also breaks coordinates scaling in mouse events // SDL_RenderSetViewport(mainRenderer, nullptr); //#endif } #if (SDL_BYTEORDER == SDL_BIG_ENDIAN) int bmask = 0xff000000; int gmask = 0x00ff0000; int rmask = 0x0000ff00; int amask = 0x000000ff; #else int bmask = 0x000000ff; int gmask = 0x0000ff00; int rmask = 0x00ff0000; int amask = 0xFF000000; #endif screen = SDL_CreateRGBSurface(0,w,h,bpp,rmask,gmask,bmask,amask); if(nullptr == screen) { logGlobal->error("Unable to create surface %dx%d with %d bpp: %s", w, h, bpp, SDL_GetError()); throw std::runtime_error("Unable to create surface"); } //No blending for screen itself. Required for proper cursor rendering. SDL_SetSurfaceBlendMode(screen, SDL_BLENDMODE_NONE); screenTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, w, h); if(nullptr == screenTexture) { logGlobal->error("Unable to create screen texture"); logGlobal->error(SDL_GetError()); throw std::runtime_error("Unable to create screen texture"); } screen2 = CSDL_Ext::copySurface(screen); if(nullptr == screen2) { throw std::runtime_error("Unable to copy surface\n"); } screenBuf = bufOnScreen ? screen : screen2; SDL_SetRenderDrawColor(mainRenderer, 0, 0, 0, 0); SDL_RenderClear(mainRenderer); SDL_RenderPresent(mainRenderer); return true; } //used only once during initialization static void setScreenRes(int w, int h, int bpp, bool fullscreen, int displayIndex, bool resetVideo) { if(!recreateWindow(w, h, bpp, fullscreen, displayIndex)) { throw std::runtime_error("Requested screen resolution is not available\n"); } } static void fullScreenChanged() { boost::unique_lock lock(*CPlayerInterface::pim); Settings full = settings.write["video"]["fullscreen"]; const bool toFullscreen = full->Bool(); auto bitsPerPixel = screen->format->BitsPerPixel; auto w = screen->w; auto h = screen->h; if(!recreateWindow(w, h, bitsPerPixel, toFullscreen, -1)) { //will return false and report error if video mode is not supported return; } GH.totalRedraw(); } static void handleEvent(SDL_Event & ev) { if((ev.type==SDL_QUIT) ||(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4 && (ev.key.keysym.mod & KMOD_ALT))) { #ifdef VCMI_ANDROID handleQuit(false); #else handleQuit(); #endif return; } #ifdef VCMI_ANDROID else if (ev.type == SDL_KEYDOWN && ev.key.keysym.scancode == SDL_SCANCODE_AC_BACK) { handleQuit(true); } #endif else if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4) { Settings full = settings.write["video"]["fullscreen"]; full->Bool() = !full->Bool(); return; } else if(ev.type == SDL_USEREVENT) { switch(ev.user.code) { case EUserEvent::FORCE_QUIT: { handleQuit(false); return; } break; case EUserEvent::RETURN_TO_MAIN_MENU: { CSH->endGameplay(); GH.defActionsDef = 63; } break; case EUserEvent::RESTART_GAME: { CSH->sendStartGame(); } break; case EUserEvent::CAMPAIGN_START_SCENARIO: { CSH->endGameplay(); auto ourCampaign = std::shared_ptr(reinterpret_cast(ev.user.data1)); auto & epilogue = ourCampaign->camp->scenarios[ourCampaign->mapsConquered.back()].epilog; auto finisher = [=]() { if(ourCampaign->mapsRemaining.size()) { CMM->openCampaignLobby(ourCampaign); } }; if(epilogue.hasPrologEpilog) { GH.pushIntT(epilogue, finisher); } else { finisher(); } } break; case EUserEvent::RETURN_TO_MENU_LOAD: CSH->endGameplay(); GH.defActionsDef = 63; CMM->menu->switchToTab("load"); break; case EUserEvent::FULLSCREEN_TOGGLED: fullScreenChanged(); break; case EUserEvent::INTERFACE_CHANGED: if(LOCPLINT) LOCPLINT->updateAmbientSounds(); break; default: logGlobal->error("Unknown user event. Code %d", ev.user.code); break; } return; } else if(ev.type == SDL_WINDOWEVENT) { switch (ev.window.event) { case SDL_WINDOWEVENT_RESTORED: fullScreenChanged(); break; } return; } //preprocessing if(ev.type == SDL_MOUSEMOTION) { CCS->curh->cursorMove(ev.motion.x, ev.motion.y); } { boost::unique_lock lock(eventsM); events.push(ev); } } static void mainLoop() { SettingsListener resChanged = settings.listen["video"]["fullscreen"]; resChanged([](const JsonNode &newState){ CGuiHandler::pushSDLEvent(SDL_USEREVENT, EUserEvent::FULLSCREEN_TOGGLED); }); inGuiThread.reset(new bool(true)); GH.mainFPSmng->init(); while(1) //main SDL events loop { SDL_Event ev; while(1 == SDL_PollEvent(&ev)) { handleEvent(ev); } CSH->applyPacksOnLobbyScreen(); GH.renderFrame(); } } void handleQuit(bool ask) { auto quitApplication = []() { if(CSH->client) CSH->endGameplay(); GH.listInt.clear(); GH.objsToBlit.clear(); CMM.reset(); // cleanup, mostly to remove false leaks from analyzer if(CCS) { CCS->musich->release(); CCS->soundh->release(); vstd::clear_pointer(CCS); } CMessage::dispose(); vstd::clear_pointer(graphics); vstd::clear_pointer(VLC); vstd::clear_pointer(console);// should be removed after everything else since used by logging boost::this_thread::sleep(boost::posix_time::milliseconds(750));//??? if(!settings["session"]["headless"].Bool()) { cleanupRenderer(); if(nullptr != mainRenderer) { SDL_DestroyRenderer(mainRenderer); mainRenderer = nullptr; } if(nullptr != mainWindow) { SDL_DestroyWindow(mainWindow); mainWindow = nullptr; } SDL_Quit(); } std::cout << "Ending...\n"; exit(0); }; if(CSH->client && LOCPLINT && ask) { CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); } else { quitApplication(); } } vcmi-0.99+git20190113.f06c8a87/client/CMT.h000066400000000000000000000013451342332007200172270ustar00rootroot00000000000000/* * CMT.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include extern SDL_Texture * screenTexture; extern SDL_Window * mainWindow; extern SDL_Renderer * mainRenderer; extern SDL_Surface *screen; // main screen surface extern SDL_Surface *screen2; // and hlp surface (used to store not-active interfaces layer) extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed void removeGUI(); void handleQuit(bool ask = true); vcmi-0.99+git20190113.f06c8a87/client/CMakeLists.txt000066400000000000000000000123221342332007200211700ustar00rootroot00000000000000set(client_SRCS StdInc.cpp ../CCallback.cpp battle/CBattleAnimations.cpp battle/CBattleInterfaceClasses.cpp battle/CBattleInterface.cpp battle/CCreatureAnimation.cpp gui/CAnimation.cpp gui/CCursorHandler.cpp gui/CGuiHandler.cpp gui/CIntObject.cpp gui/Fonts.cpp gui/Geometries.cpp gui/SDL_Extensions.cpp widgets/AdventureMapClasses.cpp widgets/Buttons.cpp widgets/CArtifactHolder.cpp widgets/CComponent.cpp widgets/CGarrisonInt.cpp widgets/Images.cpp widgets/MiscWidgets.cpp widgets/ObjectLists.cpp widgets/TextControls.cpp windows/CAdvmapInterface.cpp windows/CCastleInterface.cpp windows/CCreatureWindow.cpp windows/CreaturePurchaseCard.cpp windows/CHeroWindow.cpp windows/CKingdomInterface.cpp windows/CQuestLog.cpp windows/CSpellWindow.cpp windows/CTradeWindow.cpp windows/CWindowObject.cpp windows/GUIClasses.cpp windows/InfoWindows.cpp windows/QuickRecruitmentWindow.cpp mainmenu/CMainMenu.cpp mainmenu/CCampaignScreen.cpp mainmenu/CreditsScreen.cpp mainmenu/CPrologEpilogVideo.cpp lobby/CBonusSelection.cpp lobby/CSelectionBase.cpp lobby/CLobbyScreen.cpp lobby/CSavingScreen.cpp lobby/CScenarioInfoScreen.cpp lobby/OptionsTab.cpp lobby/RandomMapTab.cpp lobby/SelectionTab.cpp CBitmapHandler.cpp CreatureCostBox.cpp CGameInfo.cpp Client.cpp CMessage.cpp CMT.cpp CMusicHandler.cpp CPlayerInterface.cpp CVideoHandler.cpp CServerHandler.cpp Graphics.cpp mapHandler.cpp NetPacksClient.cpp NetPacksLobbyClient.cpp SDLRWwrapper.cpp ) set(client_HEADERS StdInc.h battle/CBattleAnimations.h battle/CBattleInterfaceClasses.h battle/CBattleInterface.h battle/CCreatureAnimation.h gui/CAnimation.h gui/CCursorHandler.h gui/CGuiHandler.h gui/CIntObject.h gui/Fonts.h gui/Geometries.h gui/SDL_Compat.h gui/SDL_Extensions.h gui/SDL_Pixels.h widgets/AdventureMapClasses.h widgets/Buttons.h widgets/CArtifactHolder.h widgets/CComponent.h widgets/CGarrisonInt.h widgets/Images.h widgets/MiscWidgets.h widgets/ObjectLists.h widgets/TextControls.h windows/CAdvmapInterface.h windows/CCastleInterface.h windows/CCreatureWindow.h windows/CreaturePurchaseCard.h windows/CHeroWindow.h windows/CKingdomInterface.h windows/CQuestLog.h windows/CSpellWindow.h windows/CTradeWindow.h windows/CWindowObject.h windows/GUIClasses.h windows/InfoWindows.h windows/QuickRecruitmentWindow.h mainmenu/CMainMenu.h mainmenu/CCampaignScreen.h mainmenu/CreditsScreen.h mainmenu/CPrologEpilogVideo.h lobby/CBonusSelection.h lobby/CSelectionBase.h lobby/CLobbyScreen.h lobby/CSavingScreen.h lobby/CScenarioInfoScreen.h lobby/OptionsTab.h lobby/RandomMapTab.h lobby/SelectionTab.h CBitmapHandler.h CreatureCostBox.h CGameInfo.h Client.h CMessage.h CMT.h CMusicHandler.h CPlayerInterface.h CVideoHandler.h CServerHandler.h Graphics.h mapHandler.h resource.h SDLMain.h SDLRWwrapper.h ) assign_source_group(${client_SRCS} ${client_HEADERS} VCMI_client.rc) if(ANDROID) # android needs client/server to be libraries, not executables, so we can't reuse the build part of this script return() endif() if(APPLE) set(client_SRCS ${client_SRCS} SDLMain.m) elseif(WIN32) set(client_ICON "VCMI_client.rc") endif() if(ENABLE_DEBUG_CONSOLE) add_executable(vcmiclient ${client_SRCS} ${client_HEADERS} ${client_ICON}) else() add_executable(vcmiclient WIN32 ${client_SRCS} ${client_HEADERS} ${client_ICON}) endif(ENABLE_DEBUG_CONSOLE) add_dependencies(vcmiclient vcmiserver BattleAI StupidAI VCAI) if(WIN32) set_target_properties(vcmiclient PROPERTIES OUTPUT_NAME "VCMI_client" PROJECT_LABEL "VCMI_client" ) set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclient) if(NOT ENABLE_DEBUG_CONSOLE) target_link_libraries(vcmiclient ${SDLMAIN_LIBRARY}) endif() endif() target_link_libraries(vcmiclient vcmi ${Boost_LIBRARIES} ${SDL2_LIBRARY} ${SDL2_IMAGE_LIBRARY} ${SDL2_MIXER_LIBRARY} ${SDL2_TTF_LIBRARY} ${ZLIB_LIBRARIES} ${FFMPEG_LIBRARIES} ${FFMPEG_EXTRA_LINKING_OPTIONS} ${SYSTEM_LIBS} ) target_include_directories(vcmi PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} PUBLIC ${SDL2_INCLUDE_DIR} PRIVATE ${SDL2_TTF_INCLUDE_DIR} PRIVATE ${SDL2_MIXER_INCLUDE_DIR} PRIVATE ${SDL2_IMAGE_INCLUDE_DIR} PRIVATE ${FFMPEG_INCLUDE_DIRS} ) vcmi_set_output_dir(vcmiclient "") set_target_properties(vcmiclient PROPERTIES ${PCH_PROPERTIES}) cotire(vcmiclient) install(TARGETS vcmiclient DESTINATION ${BIN_DIR}) #install icons and desktop file on Linux if(NOT WIN32 AND NOT APPLE) #FIXME: move to client makefile? install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.64x64.png" DESTINATION share/icons/hicolor/64x64/apps RENAME vcmiclient.png) install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.48x48.png" DESTINATION share/icons/hicolor/48x48/apps RENAME vcmiclient.png) install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.32x32.png" DESTINATION share/icons/hicolor/32x32/apps RENAME vcmiclient.png) install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.256x256.png" DESTINATION share/icons/hicolor/256x256/apps RENAME vcmiclient.png) install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.desktop" DESTINATION share/applications) endif() vcmi-0.99+git20190113.f06c8a87/client/CMessage.cpp000066400000000000000000000310741342332007200206300ustar00rootroot00000000000000/* * CMessage.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CMessage.h" #include "CGameInfo.h" #include "gui/SDL_Extensions.h" #include "../lib/CGeneralTextHandler.h" #include "CBitmapHandler.h" #include "gui/CAnimation.h" #include "widgets/CComponent.h" #include "windows/InfoWindows.h" #include "widgets/Buttons.h" #include "widgets/TextControls.h" const int BETWEEN_COMPS_ROWS = 10; const int BEFORE_COMPONENTS = 30; const int BETWEEN_COMPS = 30; const int SIDE_MARGIN = 30; template std::pair max(const std::pair &x, const std::pair &y) { std::pair ret; ret.first = std::max(x.first,y.first); ret.second = std::max(x.second,y.second); return ret; } //One image component + subtitles below it class ComponentResolved : public CIntObject { public: std::shared_ptr comp; //blit component with image centered at this position void showAll(SDL_Surface * to) override; //ComponentResolved(); ComponentResolved(std::shared_ptr Comp); ~ComponentResolved(); }; // Full set of components for blitting on dialog box struct ComponentsToBlit { std::vector< std::vector>> comps; int w, h; void blitCompsOnSur(bool blitOr, int inter, int &curh, SDL_Surface *ret); ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr); ~ComponentsToBlit(); }; namespace { std::array, PlayerColor::PLAYER_LIMIT_I> dialogBorders; std::array>, PlayerColor::PLAYER_LIMIT_I> piecesOfBox; SDL_Surface * background = nullptr;//todo: should be CFilledTexture } void CMessage::init() { for(int i=0; i("DIALGBOX"); dialogBorders[i]->preload(); for(int j=0; j < dialogBorders[i]->size(0); j++) { auto image = dialogBorders[i]->getImage(j, 0); //assume blue color initially if(i != 1) image->playerColored(PlayerColor(i)); piecesOfBox[i].push_back(image); } } background = BitmapHandler::loadBitmap("DIBOXBCK.BMP"); } void CMessage::dispose() { for(auto & item : dialogBorders) item.reset(); SDL_FreeSurface(background); } SDL_Surface * CMessage::drawDialogBox(int w, int h, PlayerColor playerColor) { //prepare surface SDL_Surface * ret = SDL_CreateRGBSurface(0, w, h, screen->format->BitsPerPixel, screen->format->Rmask, screen->format->Gmask, screen->format->Bmask, screen->format->Amask); for (int i=0; iw)//background { for (int j=0; jh) { Rect srcR(0,0,background->w, background->h); Rect dstR(i,j,w,h); CSDL_Ext::blitSurface(background, &srcR, ret, &dstR); } } drawBorder(playerColor, ret, w, h); return ret; } std::vector CMessage::breakText( std::string text, size_t maxLineWidth, EFonts font ) { std::vector ret; boost::algorithm::trim_right_if(text,boost::algorithm::is_any_of(std::string(" "))); // each iteration generates one output line while (text.length()) { ui32 lineWidth = 0; //in characters or given char metric ui32 wordBreak = -1; //last position for line break (last space character) ui32 currPos = 0; //current position in text bool opened = false; //set to true when opening brace is found size_t symbolSize = 0; // width of character, in bytes size_t glyphWidth = 0; // width of printable glyph, pixels // loops till line is full or end of text reached while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth) { symbolSize = Unicode::getCharacterSize(text[currPos]); glyphWidth = graphics->fonts[font]->getGlyphWidth(text.data() + currPos); // candidate for line break if (ui8(text[currPos]) <= ui8(' ')) wordBreak = currPos; /* We don't count braces in string length. */ if (text[currPos] == '{') opened=true; else if (text[currPos]=='}') opened=false; else lineWidth += glyphWidth; currPos += symbolSize; } // long line, create line break if (currPos < text.length() && (text[currPos] != 0x0a)) { if (wordBreak != ui32(-1)) currPos = wordBreak; else currPos -= symbolSize; } //non-blank line if(currPos != 0) { ret.push_back(text.substr(0, currPos)); if (opened) /* Close the brace for the current line. */ ret.back() += '}'; text.erase(0, currPos); } else if(text[currPos] == 0x0a) { ret.push_back(""); //add empty string, no extra actions needed } if (text.length() != 0 && text[0] == 0x0a) { /* Remove LF */ text.erase(0, 1); } else { // trim only if line does not starts with LF // FIXME: necessary? All lines will be trimmed before returning anyway boost::algorithm::trim_left_if(text,boost::algorithm::is_any_of(std::string(" "))); } if (opened) { /* Add an opening brace for the next line. */ if (text.length() != 0) text.insert(0, "{"); } } /* Trim whitespaces of every line. */ for (auto & elem : ret) boost::algorithm::trim(elem); return ret; } void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player) { bool blitOr = false; if(dynamic_cast(ret)) //it's selection window, so we'll blit "or" between components blitOr = true; const int sizes[][2] = {{400, 125}, {500, 150}, {600, 200}, {480, 400}}; for(int i = 0; i < ARRAY_COUNT(sizes) && sizes[i][0] < screen->w - 150 && sizes[i][1] < screen->h - 150 && ret->text->slider; i++) { ret->text->resize(Point(sizes[i][0], sizes[i][1])); } if(ret->text->slider) { ret->text->slider->addUsedEvents(CIntObject::WHEEL | CIntObject::KEYBOARD); } else { ret->text->resize(ret->text->label->textSize + Point(10, 10)); } std::pair winSize(ret->text->pos.w, ret->text->pos.h); //start with text size ComponentsToBlit comps(ret->components,500, blitOr); if (ret->components.size()) winSize.second += 10 + comps.h; //space to first component int bw = 0; if (ret->buttons.size()) { int bh = 0; // Compute total width of buttons bw = 20*(ret->buttons.size()-1); // space between all buttons for(auto & elem : ret->buttons) //and add buttons width { bw+=elem->pos.w; vstd::amax(bh, elem->pos.h); } winSize.second += 20 + bh;//before button + button } // Clip window size vstd::amax(winSize.second, 50); vstd::amax(winSize.first, 80); vstd::amax(winSize.first, comps.w); vstd::amax(winSize.first, bw); vstd::amin(winSize.first, screen->w - 150); ret->bitmap = drawDialogBox (winSize.first + 2*SIDE_MARGIN, winSize.second + 2*SIDE_MARGIN, player); ret->pos.h=ret->bitmap->h; ret->pos.w=ret->bitmap->w; ret->center(); int curh = SIDE_MARGIN; int xOffset = (ret->pos.w - ret->text->pos.w)/2; if(!ret->buttons.size() && !ret->components.size()) //improvement for very small text only popups -> center text vertically { if(ret->bitmap->h > ret->text->pos.h + 2*SIDE_MARGIN) curh = (ret->bitmap->h - ret->text->pos.h)/2; } ret->text->moveBy(Point(xOffset, curh)); curh += ret->text->pos.h; if (ret->components.size()) { curh += BEFORE_COMPONENTS; comps.blitCompsOnSur (blitOr, BETWEEN_COMPS, curh, ret->bitmap); } if(ret->buttons.size()) { // Position the buttons at the bottom of the window bw = (ret->bitmap->w/2) - (bw/2); curh = ret->bitmap->h - SIDE_MARGIN - ret->buttons[0]->pos.h; for(auto & elem : ret->buttons) { elem->moveBy(Point(bw, curh)); bw += elem->pos.w + 20; } } for(size_t i=0; icomponents.size(); i++) ret->components[i]->moveBy(Point(ret->pos.x, ret->pos.y)); } void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y) { if(playerColor.isSpectator()) playerColor = PlayerColor(1); auto & box = piecesOfBox.at(playerColor.getNum()); // Note: this code assumes that the corner dimensions are all the same. // Horizontal borders int start_x = x + box[0]->width(); const int stop_x = x + w - box[1]->width(); const int bottom_y = y+h-box[7]->height()+1; while (start_x < stop_x) { int cur_w = stop_x - start_x; if (cur_w > box[6]->width()) cur_w = box[6]->width(); // Top border Rect srcR(0, 0, cur_w, box[6]->height()); Rect dstR(start_x, y, 0, 0); box[6]->draw(ret, &dstR, &srcR); // Bottom border dstR.y = bottom_y; box[7]->draw(ret, &dstR, &srcR); start_x += cur_w; } // Vertical borders int start_y = y + box[0]->height(); const int stop_y = y + h - box[2]->height()+1; const int right_x = x+w-box[5]->width(); while (start_y < stop_y) { int cur_h = stop_y - start_y; if (cur_h > box[4]->height()) cur_h = box[4]->height(); // Left border Rect srcR(0, 0, box[4]->width(), cur_h); Rect dstR(x, start_y, 0, 0); box[4]->draw(ret, &dstR, &srcR); // Right border dstR.x = right_x; box[5]->draw(ret, &dstR, &srcR); start_y += cur_h; } //corners Rect dstR(x, y, box[0]->width(), box[0]->height()); box[0]->draw(ret, &dstR, nullptr); dstR=Rect(x+w-box[1]->width(), y, box[1]->width(), box[1]->height()); box[1]->draw(ret, &dstR, nullptr); dstR=Rect(x, y+h-box[2]->height()+1, box[2]->width(), box[2]->height()); box[2]->draw(ret, &dstR, nullptr); dstR=Rect(x+w-box[3]->width(), y+h-box[3]->height()+1, box[3]->width(), box[3]->height()); box[3]->draw(ret, &dstR, nullptr); } ComponentResolved::ComponentResolved(std::shared_ptr Comp): comp(Comp) { //Temporary assign ownership on comp if (parent) parent->removeChild(this); if (comp->parent) { comp->parent->addChild(this); comp->parent->removeChild(comp.get()); } addChild(comp.get()); defActions = 255 - DISPOSE; pos.x = pos.y = 0; pos.w = comp->pos.w; pos.h = comp->pos.h; } ComponentResolved::~ComponentResolved() { if (parent) { removeChild(comp.get()); parent->addChild(comp.get()); } } void ComponentResolved::showAll(SDL_Surface *to) { CIntObject::showAll(to); comp->showAll(to); } ComponentsToBlit::~ComponentsToBlit() = default; ComponentsToBlit::ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr) { int orWidth = graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4]); w = h = 0; if(SComps.empty()) return; comps.resize(1); int curw = 0; int curr = 0; //current row for(auto & SComp : SComps) { auto cur = std::make_shared(SComp); int toadd = (cur->pos.w + BETWEEN_COMPS + (blitOr ? orWidth : 0)); if (curw + toadd > maxw) { curr++; vstd::amax(w,curw); curw = cur->pos.w; comps.resize(curr+1); } else { curw += toadd; vstd::amax(w,curw); } comps[curr].push_back(cur); } for(auto & elem : comps) { int maxHeight = 0; for(size_t j=0;jpos.h); h += maxHeight + BETWEEN_COMPS_ROWS; } } void ComponentsToBlit::blitCompsOnSur( bool blitOr, int inter, int &curh, SDL_Surface *ret ) { int orWidth = graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4]); for (auto & elem : comps)//for each row { int totalw=0, maxHeight=0; for(size_t j=0;jpos.w; vstd::amax(maxHeight, cur->pos.h); } //add space between comps in this row if(blitOr) totalw += (inter*2+orWidth) * (elem.size() - 1); else totalw += (inter) * (elem.size() - 1); int middleh = curh + maxHeight/2;//axis for image aligment int curw = ret->w/2 - totalw/2; for(size_t j=0;jmoveTo(Point(curw, curh)); //blit component cur->showAll(ret); curw += cur->pos.w; //if there is subsequent component blit "or" if(j<(elem.size()-1)) { if(blitOr) { curw+=inter; graphics->fonts[FONT_MEDIUM]->renderTextLeft(ret, CGI->generaltexth->allTexts[4], Colors::WHITE, Point(curw,middleh-(graphics->fonts[FONT_MEDIUM]->getLineHeight()/2))); curw+=orWidth; } curw+=inter; } } curh += maxHeight + BETWEEN_COMPS_ROWS; } } vcmi-0.99+git20190113.f06c8a87/client/CMessage.h000066400000000000000000000020441342332007200202700ustar00rootroot00000000000000/* * CMessage.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "Graphics.h" #include "gui/Geometries.h" struct SDL_Surface; class CInfoWindow; class CComponent; /// Class which draws formatted text messages and generates chat windows class CMessage { /// Draw simple dialog box (borders and background only) static SDL_Surface * drawDialogBox(int w, int h, PlayerColor playerColor = PlayerColor(1)); public: /// Draw border on exiting surface static void drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x=0, int y=0); static void drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player); /// split text in lines static std::vector breakText(std::string text, size_t maxLineWidth, EFonts font); /// constructor static void init(); /// destructor static void dispose(); }; vcmi-0.99+git20190113.f06c8a87/client/CMusicHandler.cpp000066400000000000000000000334631342332007200216260ustar00rootroot00000000000000/* * CMusicHandler.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include #include "CMusicHandler.h" #include "CGameInfo.h" #include "SDLRWwrapper.h" #include "../lib/CCreatureHandler.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/JsonNode.h" #include "../lib/GameConstants.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/StringConstants.h" #include "../lib/CRandomGenerator.h" #include "../lib/VCMIDirs.h" #define VCMI_SOUND_NAME(x) #define VCMI_SOUND_FILE(y) #y, // sounds mapped to soundBase enum static std::string sounds[] = { "", // invalid "", // todo VCMI_SOUND_LIST }; #undef VCMI_SOUND_NAME #undef VCMI_SOUND_FILE // Not pretty, but there's only one music handler object in the game. static void soundFinishedCallbackC(int channel) { CCS->soundh->soundFinishedCallback(channel); } static void musicFinishedCallbackC() { CCS->musich->musicFinishedCallback(); } void CAudioBase::init() { if (initialized) return; if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1) { logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError()); return; } initialized = true; } void CAudioBase::release() { if (initialized) { Mix_CloseAudio(); initialized = false; } } void CAudioBase::setVolume(ui32 percent) { if (percent > 100) percent = 100; volume = percent; } void CSoundHandler::onVolumeChange(const JsonNode &volumeNode) { setVolume(volumeNode.Float()); } CSoundHandler::CSoundHandler(): listener(settings.listen["general"]["sound"]), ambientConfig(JsonNode(ResourceID("config/ambientSounds.json"))) { allTilesSource = ambientConfig["allTilesSource"].Bool(); listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); // Vectors for helper(s) pickupSounds = { soundBase::pickup01, soundBase::pickup02, soundBase::pickup03, soundBase::pickup04, soundBase::pickup05, soundBase::pickup06, soundBase::pickup07 }; horseSounds = // must be the same order as terrains (see ETerrainType); { soundBase::horseDirt, soundBase::horseSand, soundBase::horseGrass, soundBase::horseSnow, soundBase::horseSwamp, soundBase::horseRough, soundBase::horseSubterranean, soundBase::horseLava, soundBase::horseWater, soundBase::horseRock }; battleIntroSounds = { soundBase::battle00, soundBase::battle01, soundBase::battle02, soundBase::battle03, soundBase::battle04, soundBase::battle05, soundBase::battle06, soundBase::battle07 }; }; void CSoundHandler::init() { CAudioBase::init(); if(ambientConfig["allocateChannels"].isNumber()) Mix_AllocateChannels(ambientConfig["allocateChannels"].Integer()); if (initialized) { // Load sounds Mix_ChannelFinished(soundFinishedCallbackC); } } void CSoundHandler::release() { if (initialized) { Mix_HaltChannel(-1); for (auto &chunk : soundChunks) { if (chunk.second.first) Mix_FreeChunk(chunk.second.first); } } CAudioBase::release(); } // Allocate an SDL chunk and cache it. Mix_Chunk *CSoundHandler::GetSoundChunk(std::string &sound, bool cache) { try { if (cache && soundChunks.find(sound) != soundChunks.end()) return soundChunks[sound].first; auto data = CResourceHandler::get()->load(ResourceID(std::string("SOUNDS/") + sound, EResType::SOUND))->readAll(); SDL_RWops *ops = SDL_RWFromMem(data.first.get(), data.second); Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops if (cache) soundChunks.insert(std::pair(sound, std::make_pair (chunk, std::move (data.first)))); return chunk; } catch(std::exception &e) { logGlobal->warn("Cannot get sound %s chunk: %s", sound, e.what()); return nullptr; } } int CSoundHandler::ambientDistToVolume(int distance) const { if(distance >= ambientConfig["distances"].Vector().size()) return 0; int volume = ambientConfig["distances"].Vector()[distance].Integer(); return volume * ambientConfig["volume"].Integer() * getVolume() / 10000; } void CSoundHandler::ambientStopSound(std::string soundId) { stopSound(ambientChannels[soundId]); setChannelVolume(ambientChannels[soundId], 100); } // Plays a sound, and return its channel so we can fade it out later int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) { assert(soundID < soundBase::sound_after_last); auto sound = sounds[soundID]; logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound); return playSound(sound, repeats, true); } int CSoundHandler::playSound(std::string sound, int repeats, bool cache) { if (!initialized || sound.empty()) return -1; int channel; Mix_Chunk *chunk = GetSoundChunk(sound, cache); if (chunk) { channel = Mix_PlayChannel(-1, chunk, repeats); if (channel == -1) { logGlobal->error("Unable to play sound file %s , error %s", sound, Mix_GetError()); if (!cache) Mix_FreeChunk(chunk); } else if (cache) callbacks[channel]; else callbacks[channel] = [chunk](){ Mix_FreeChunk(chunk);}; } else channel = -1; return channel; } // Helper. Randomly select a sound from an array and play it int CSoundHandler::playSoundFromSet(std::vector &sound_vec) { return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault())); } void CSoundHandler::stopSound( int handler ) { if (initialized && handler != -1) Mix_HaltChannel(handler); } // Sets the sound volume, from 0 (mute) to 100 void CSoundHandler::setVolume(ui32 percent) { CAudioBase::setVolume(percent); if (initialized) setChannelVolume(-1, volume); } // Sets the sound volume, from 0 (mute) to 100 void CSoundHandler::setChannelVolume(int channel, ui32 percent) { Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100); } void CSoundHandler::setCallback(int channel, std::function function) { std::map >::iterator iter; iter = callbacks.find(channel); //channel not found. It may have finished so fire callback now if(iter == callbacks.end()) function(); else iter->second = function; } void CSoundHandler::soundFinishedCallback(int channel) { std::map >::iterator iter; iter = callbacks.find(channel); assert(iter != callbacks.end()); if (iter->second) iter->second(); callbacks.erase(iter); } int CSoundHandler::ambientGetRange() const { return ambientConfig["range"].Integer(); } bool CSoundHandler::ambientCheckVisitable() const { return !allTilesSource; } void CSoundHandler::ambientUpdateChannels(std::map soundsArg) { boost::mutex::scoped_lock guard(mutex); std::vector stoppedSounds; for(auto & pair : ambientChannels) { if(!vstd::contains(soundsArg, pair.first)) { ambientStopSound(pair.first); stoppedSounds.push_back(pair.first); } else { CCS->soundh->setChannelVolume(pair.second, ambientDistToVolume(soundsArg[pair.first])); } } for(auto soundId : stoppedSounds) ambientChannels.erase(soundId); for(auto & pair : soundsArg) { if(!vstd::contains(ambientChannels, pair.first)) { int channel = CCS->soundh->playSound(pair.first, -1); CCS->soundh->setChannelVolume(channel, ambientDistToVolume(pair.second)); CCS->soundh->ambientChannels.insert(std::make_pair(pair.first, channel)); } } } void CSoundHandler::ambientStopAllChannels() { boost::mutex::scoped_lock guard(mutex); for(auto ch : ambientChannels) { ambientStopSound(ch.first); } ambientChannels.clear(); } void CMusicHandler::onVolumeChange(const JsonNode &volumeNode) { setVolume(volumeNode.Float()); } CMusicHandler::CMusicHandler(): listener(settings.listen["general"]["music"]) { listener(std::bind(&CMusicHandler::onVolumeChange, this, _1)); auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourceID & id) -> bool { if(id.getType() != EResType::MUSIC) return false; if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/")) return false; logGlobal->trace("Found music file %s", id.getName()); return true; }); int battleMusicID = 0; int AIThemeID = 0; for(const ResourceID & file : mp3files) { if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) addEntryToSet("battle", battleMusicID++, file.getName()); else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme")) addEntryToSet("enemy-turn", AIThemeID++, file.getName()); } JsonNode terrains(ResourceID("config/terrains.json")); for (auto entry : terrains.Struct()) { int terrIndex = vstd::find_pos(GameConstants::TERRAIN_NAMES, entry.first); addEntryToSet("terrain", terrIndex, "Music/" + entry.second["music"].String()); } } void CMusicHandler::addEntryToSet(std::string set, int musicID, std::string musicURI) { musicsSet[set][musicID] = musicURI; } void CMusicHandler::init() { CAudioBase::init(); if (initialized) Mix_HookMusicFinished(musicFinishedCallbackC); } void CMusicHandler::release() { if (initialized) { boost::mutex::scoped_lock guard(mutex); Mix_HookMusicFinished(nullptr); current.reset(); next.reset(); } CAudioBase::release(); } void CMusicHandler::playMusic(std::string musicURI, bool loop) { if (current && current->isTrack(musicURI)) return; queueNext(this, "", musicURI, loop); } void CMusicHandler::playMusicFromSet(std::string whichSet, bool loop) { auto selectedSet = musicsSet.find(whichSet); if (selectedSet == musicsSet.end()) { logGlobal->error("Error: playing music from non-existing set: %s", whichSet); return; } if (current && current->isSet(whichSet)) return; // in this mode - play random track from set queueNext(this, whichSet, "", loop); } void CMusicHandler::playMusicFromSet(std::string whichSet, int entryID, bool loop) { auto selectedSet = musicsSet.find(whichSet); if (selectedSet == musicsSet.end()) { logGlobal->error("Error: playing music from non-existing set: %s", whichSet); return; } auto selectedEntry = selectedSet->second.find(entryID); if (selectedEntry == selectedSet->second.end()) { logGlobal->error("Error: playing non-existing entry %d from set: %s", entryID, whichSet); return; } if (current && current->isTrack(selectedEntry->second)) return; // in this mode - play specific track from set queueNext(this, "", selectedEntry->second, loop); } void CMusicHandler::queueNext(std::unique_ptr queued) { if (!initialized) return; boost::mutex::scoped_lock guard(mutex); next = std::move(queued); if (current.get() == nullptr || !current->stop(1000)) { current.reset(next.release()); current->play(); } } void CMusicHandler::queueNext(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped) { try { queueNext(make_unique(owner, setName, musicURI, looped)); } catch(std::exception &e) { logGlobal->error("Failed to queue music. setName=%s\tmusicURI=%s", setName, musicURI); logGlobal->error("Exception: %s", e.what()); } } void CMusicHandler::stopMusic(int fade_ms) { if (!initialized) return; boost::mutex::scoped_lock guard(mutex); if (current.get() != nullptr) current->stop(fade_ms); next.reset(); } void CMusicHandler::setVolume(ui32 percent) { CAudioBase::setVolume(percent); if (initialized) Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100); } void CMusicHandler::musicFinishedCallback() { boost::mutex::scoped_lock guard(mutex); if (current.get() != nullptr) { //return if current music still not finished if (current->play()) return; else current.reset(); } if (current.get() == nullptr && next.get() != nullptr) { current.reset(next.release()); current->play(); } } MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped): owner(owner), music(nullptr), loop(looped ? -1 : 1), setName(std::move(setName)) { if (!musicURI.empty()) load(std::move(musicURI)); } MusicEntry::~MusicEntry() { logGlobal->trace("Del-ing music file %s", currentName); if (music) Mix_FreeMusic(music); } void MusicEntry::load(std::string musicURI) { if (music) { logGlobal->trace("Del-ing music file %s", currentName); Mix_FreeMusic(music); music = nullptr; } currentName = musicURI; logGlobal->trace("Loading music file %s", musicURI); auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(ResourceID(std::move(musicURI), EResType::MUSIC))); music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); if(!music) { logGlobal->warn("Warning: Cannot open %s: %s", currentName, Mix_GetError()); return; } } bool MusicEntry::play() { if (!(loop--) && music) //already played once - return return false; if (!setName.empty()) { auto set = owner->musicsSet[setName]; load(RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault())->second); } logGlobal->trace("Playing music file %s", currentName); if(Mix_PlayMusic(music, 1) == -1) { logGlobal->error("Unable to play music (%s)", Mix_GetError()); return false; } return true; } bool MusicEntry::stop(int fade_ms) { if (Mix_PlayingMusic()) { logGlobal->trace("Stopping music file %s", currentName); loop = 0; Mix_FadeOutMusic(fade_ms); return true; } return false; } bool MusicEntry::isSet(std::string set) { return !setName.empty() && set == setName; } bool MusicEntry::isTrack(std::string track) { return setName.empty() && track == currentName; } vcmi-0.99+git20190113.f06c8a87/client/CMusicHandler.h000066400000000000000000000102301342332007200212560ustar00rootroot00000000000000/* * CMusicHandler.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../lib/CConfigHandler.h" #include "../lib/CSoundBase.h" class CSpell; struct _Mix_Music; struct SDL_RWops; typedef struct _Mix_Music Mix_Music; struct Mix_Chunk; class CAudioBase { protected: boost::mutex mutex; bool initialized; int volume; // from 0 (mute) to 100 public: CAudioBase(): initialized(false), volume(0) {}; virtual void init() = 0; virtual void release() = 0; virtual void setVolume(ui32 percent); ui32 getVolume() const { return volume; }; }; class CSoundHandler: public CAudioBase { private: //soundBase::soundID getSoundID(const std::string &fileName); //update volume on configuration change SettingsListener listener; void onVolumeChange(const JsonNode &volumeNode); using CachedChunk = std::pair>; std::map soundChunks; Mix_Chunk *GetSoundChunk(std::string &sound, bool cache); //have entry for every currently active channel //std::function will be nullptr if callback was not set std::map > callbacks; int ambientDistToVolume(int distance) const; void ambientStopSound(std::string soundId); const JsonNode ambientConfig; bool allTilesSource; std::map ambientChannels; public: CSoundHandler(); void init() override; void release() override; void setVolume(ui32 percent) override; void setChannelVolume(int channel, ui32 percent); // Sounds int playSound(soundBase::soundID soundID, int repeats=0); int playSound(std::string sound, int repeats=0, bool cache=false); int playSoundFromSet(std::vector &sound_vec); void stopSound(int handler); void setCallback(int channel, std::function function); void soundFinishedCallback(int channel); int ambientGetRange() const; bool ambientCheckVisitable() const; void ambientUpdateChannels(std::map currentSounds); void ambientStopAllChannels(); // Sets std::vector pickupSounds; std::vector horseSounds; std::vector battleIntroSounds; }; // Helper //now it looks somewhat useless #define battle_sound(creature,what_sound) creature->sounds.what_sound class CMusicHandler; //Class for handling one music file class MusicEntry { CMusicHandler *owner; Mix_Music *music; int loop; // -1 = indefinite //if not null - set from which music will be randomly selected std::string setName; std::string currentName; void load(std::string musicURI); public: bool isSet(std::string setName); bool isTrack(std::string trackName); MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped); ~MusicEntry(); bool play(); bool stop(int fade_ms=0); }; class CMusicHandler: public CAudioBase { private: //update volume on configuration change SettingsListener listener; void onVolumeChange(const JsonNode &volumeNode); std::unique_ptr current; std::unique_ptr next; void queueNext(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped); void queueNext(std::unique_ptr queued); std::map > musicsSet; public: CMusicHandler(); /// add entry with URI musicURI in set. Track will have ID musicID void addEntryToSet(std::string set, int musicID, std::string musicURI); void init() override; void release() override; void setVolume(ui32 percent) override; /// play track by URI, if loop = true music will be looped void playMusic(std::string musicURI, bool loop); /// play random track from this set void playMusicFromSet(std::string musicSet, bool loop); /// play specific track from set void playMusicFromSet(std::string musicSet, int entryID, bool loop); void stopMusic(int fade_ms=1000); void musicFinishedCallback(); friend class MusicEntry; }; vcmi-0.99+git20190113.f06c8a87/client/CPlayerInterface.cpp000066400000000000000000002724641342332007200223330ustar00rootroot00000000000000/* * CPlayerInterface.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "windows/CAdvmapInterface.h" #include "battle/CBattleInterface.h" #include "battle/CBattleInterfaceClasses.h" #include "../CCallback.h" #include "windows/CCastleInterface.h" #include "gui/CCursorHandler.h" #include "windows/CKingdomInterface.h" #include "CGameInfo.h" #include "windows/CHeroWindow.h" #include "windows/CCreatureWindow.h" #include "windows/CQuestLog.h" #include "CMessage.h" #include "CPlayerInterface.h" #include "gui/SDL_Extensions.h" #include "widgets/CComponent.h" #include "windows/CTradeWindow.h" #include "windows/CSpellWindow.h" #include "../lib/CConfigHandler.h" #include "battle/CCreatureAnimation.h" #include "Graphics.h" #include "windows/GUIClasses.h" #include "../lib/CArtHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/serializer/CTypeList.h" #include "../lib/serializer/BinaryDeserializer.h" #include "../lib/serializer/BinarySerializer.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/CTownHandler.h" #include "../lib/mapObjects/CObjectClassesHandler.h" // For displaying correct UI when interacting with objects #include "../lib/CStack.h" #include "../lib/JsonNode.h" #include "CMusicHandler.h" #include "../lib/CondSh.h" #include "../lib/NetPacksBase.h" #include "../lib/NetPacks.h"//todo: remove #include "../lib/mapping/CMap.h" #include "../lib/VCMIDirs.h" #include "mapHandler.h" #include "../lib/CStopWatch.h" #include "../lib/StartInfo.h" #include "../lib/CPlayerState.h" #include "../lib/GameConstants.h" #include "gui/CGuiHandler.h" #include "windows/InfoWindows.h" #include "../lib/UnlockGuard.h" #include "../lib/CPathfinder.h" #include #include "CServerHandler.h" // FIXME: only needed for CGameState::mutex #include "../lib/CGameState.h" // The macro below is used to mark functions that are called by client when game state changes. // They all assume that CPlayerInterface::pim mutex is locked. #define EVENT_HANDLER_CALLED_BY_CLIENT // The macro marks functions that are run on a new thread by client. // They do not own any mutexes intiially. #define THREAD_CREATED_BY_CLIENT #define RETURN_IF_QUICK_COMBAT \ if (isAutoFightOn && !battleInt) \ return; #define BATTLE_EVENT_POSSIBLE_RETURN\ if (LOCPLINT != this) \ return; \ RETURN_IF_QUICK_COMBAT using namespace CSDL_Ext; void processCommand(const std::string &message, CClient *&client); extern std::queue events; extern boost::mutex eventsM; boost::recursive_mutex * CPlayerInterface::pim = new boost::recursive_mutex; CPlayerInterface * LOCPLINT; CBattleInterface * CPlayerInterface::battleInt; enum EMoveState {STOP_MOVE, WAITING_MOVE, CONTINUE_MOVE, DURING_MOVE}; CondSh stillMoveHero(STOP_MOVE); //used during hero movement static bool objectBlitOrderSorter(const TerrainTileObject & a, const TerrainTileObject & b) { return CMapHandler::compareObjectBlitOrder(a.obj, b.obj); } struct HeroObjectRetriever : boost::static_visitor { const CGHeroInstance * operator()(const ConstTransitivePtr &h) const { return h; } const CGHeroInstance * operator()(const ConstTransitivePtr &s) const { return nullptr; } }; CPlayerInterface::CPlayerInterface(PlayerColor Player) { logGlobal->trace("\tHuman player interface for player %s being constructed", Player.getStr()); destinationTeleport = ObjectInstanceID(); destinationTeleportPos = int3(-1); GH.defActionsDef = 0; LOCPLINT = this; curAction = nullptr; playerID=Player; human=true; currentSelection = nullptr; battleInt = nullptr; castleInt = nullptr; makingTurn = false; showingDialog = new CondSh(false); cingconsole = new CInGameConsole(); GH.terminate_cond->set(false); firstCall = 1; //if loading will be overwritten in serialize autosaveCount = 0; isAutoFightOn = false; duringMovement = false; ignoreEvents = false; } CPlayerInterface::~CPlayerInterface() { CCS->soundh->ambientStopAllChannels(); logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.getStr()); delete showingDialog; delete cingconsole; if (LOCPLINT == this) LOCPLINT = nullptr; } void CPlayerInterface::init(std::shared_ptr CB) { cb = CB; initializeHeroTownList(); // always recreate advmap interface to avoid possible memory-corruption bugs adventureInt.reset(new CAdvMapInt()); } void CPlayerInterface::yourTurn() { EVENT_HANDLER_CALLED_BY_CLIENT; { boost::unique_lock lock(eventsM); //block handling events until interface is ready LOCPLINT = this; GH.curInt = this; adventureInt->selection = nullptr; std::string prefix = settings["session"]["saveprefix"].String(); int frequency = settings["general"]["saveFrequency"].Integer(); if (firstCall) { if(CSH->howManyPlayerInterfaces() == 1) adventureInt->setPlayer(playerID); autosaveCount = getLastIndex(prefix + "Autosave_"); if (firstCall > 0) //new game, not loaded { int index = getLastIndex(prefix + "Newgame_"); index %= SAVES_COUNT; cb->save("Saves/" + prefix + "Newgame_Autosave_" + boost::lexical_cast(index + 1)); } firstCall = 0; } else if(frequency > 0 && cb->getDate() % frequency == 0) { LOCPLINT->cb->save("Saves/" + prefix + "Autosave_" + boost::lexical_cast(autosaveCount++ + 1)); autosaveCount %= 5; } if (adventureInt->player != playerID) adventureInt->setPlayer(playerID); if (CSH->howManyPlayerInterfaces() > 1) //hot seat message { adventureInt->startHotSeatWait(playerID); makingTurn = true; std::string msg = CGI->generaltexth->allTexts[13]; boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); std::vector> cmp; cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); showInfoDialog(msg, cmp); } else { makingTurn = true; adventureInt->startTurn(); } } acceptTurn(); } STRONG_INLINE void subRect(const int & x, const int & y, const int & z, const SDL_Rect & r, const ObjectInstanceID & hid) { TerrainTile2 & hlp = CGI->mh->ttiles[x][y][z]; for (auto & elem : hlp.objects) if (elem.obj && elem.obj->id == hid) { elem.rect = r; return; } } STRONG_INLINE void delObjRect(const int & x, const int & y, const int & z, const ObjectInstanceID & hid) { TerrainTile2 & hlp = CGI->mh->ttiles[x][y][z]; for (int h=0; hid == hid) { hlp.objects.erase(hlp.objects.begin()+h); return; } } void CPlayerInterface::heroMoved(const TryMoveHero & details) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); if (LOCPLINT != this) return; if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool()) return; const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero int3 hp = details.start; if (!hero) { //AI hero left the visible area (we can't obtain info) //TODO very evil workaround -> retrieve pointer to hero so we could animate it // TODO -> we should not need full CGHeroInstance structure to display animation or it should not be handled by playerint (but by the client itself) const TerrainTile2 &tile = CGI->mh->ttiles[hp.x-1][hp.y][hp.z]; for (auto & elem : tile.objects) if (elem.obj && elem.obj->id == details.id) hero = dynamic_cast(elem.obj); if (!hero) //still nothing... return; } bool directlyAttackingCreature = details.attackedFrom && adventureInt->terrain.currentPath //in case if movement has been canceled in the meantime and path was already erased && adventureInt->terrain.currentPath->nodes.size() == 3;//FIXME should be 2 but works nevertheless... if (makingTurn && hero->tempOwner == playerID) //we are moving our hero - we may need to update assigned path { updateAmbientSounds(); //We may need to change music - select new track, music handler will change it if needed CCS->musich->playMusicFromSet("terrain", LOCPLINT->cb->getTile(hero->visitablePos())->terType, true); if (details.result == TryMoveHero::TELEPORTATION) { if (adventureInt->terrain.currentPath) { assert(adventureInt->terrain.currentPath->nodes.size() >= 2); std::vector::const_iterator nodesIt = adventureInt->terrain.currentPath->nodes.end() - 1; if ((nodesIt)->coord == CGHeroInstance::convertPosition(details.start, false) && (nodesIt-1)->coord == CGHeroInstance::convertPosition(details.end, false)) { //path was between entrance and exit of teleport -> OK, erase node as usual removeLastNodeFromPath(hero); } else { //teleport was not along current path, it'll now be invalid (hero is somewhere else) eraseCurrentPathOf(hero); } } adventureInt->centerOn(hero, true); //actualizing screen pos adventureInt->minimap.redraw(); adventureInt->heroList.update(hero); return; //teleport - no fancy moving animation //TODO: smooth disappear / appear effect } if (hero->pos != details.end //hero didn't change tile but visit succeeded || directlyAttackingCreature) // or creature was attacked from endangering tile. { eraseCurrentPathOf(hero, false); } else if (adventureInt->terrain.currentPath && hero->pos == details.end) //&& hero is moving { if (details.start != details.end) //so we don't touch path when revisiting with spacebar removeLastNodeFromPath(hero); } } if(details.stopMovement()) //hero failed to move { hero->isStanding = true; stillMoveHero.setn(STOP_MOVE); GH.totalRedraw(); adventureInt->heroList.update(hero); return; } ui32 speed = 0; if(settings["session"]["spectate"].Bool()) { if(!settings["session"]["spectate-hero-speed"].isNull()) speed = settings["session"]["spectate-hero-speed"].Integer(); } else if (makingTurn) // our turn, our hero moves speed = settings["adventure"]["heroSpeed"].Float(); else speed = settings["adventure"]["enemySpeed"].Float(); if (speed == 0) { //FIXME: is this a proper solution? CGI->mh->hideObject(hero); CGI->mh->printObject(hero); return; // no animation } adventureInt->centerOn(hero); //actualizing screen pos adventureInt->minimap.redraw(); adventureInt->heroList.redraw(); initMovement(details, hero, hp); //first initializing done GH.mainFPSmng->framerateDelay(); // after first move //main moving for (int i=1; i<32; i+=2*speed) { movementPxStep(details, i, hp, hero); #ifndef VCMI_ANDROID // currently android doesn't seem to be able to handle all these full redraws here, so let's disable it so at least it looks less choppy; // most likely this is connected with the way that this manual animation+framerate handling is solved adventureInt->updateScreen = true; #endif adventureInt->show(screen); { //evil returns here ... //todo: get rid of it logGlobal->trace("before [un]locks in %s", __FUNCTION__); auto unlockPim = vstd::makeUnlockGuard(*pim); //let frame to be rendered GH.mainFPSmng->framerateDelay(); //for animation purposes logGlobal->trace("after [un]locks in %s", __FUNCTION__); } } //main moving done //finishing move finishMovement(details, hp, hero); hero->isStanding = true; //move finished adventureInt->minimap.redraw(); adventureInt->heroList.update(hero); //check if user cancelled movement { boost::unique_lock un(eventsM); while(!events.empty()) { SDL_Event ev = events.front(); events.pop(); switch(ev.type) { case SDL_MOUSEBUTTONDOWN: stillMoveHero.setn(STOP_MOVE); break; case SDL_KEYDOWN: if (ev.key.keysym.sym < SDLK_F1 || ev.key.keysym.sym > SDLK_F15) stillMoveHero.setn(STOP_MOVE); break; } } } if (stillMoveHero.get() == WAITING_MOVE) stillMoveHero.setn(DURING_MOVE); // Hero attacked creature directly, set direction to face it. if (directlyAttackingCreature) { // Get direction to attacker. int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0); static const ui8 dirLookup[3][3] = { { 1, 2, 3 }, { 8, 0, 4 }, { 7, 6, 5 } }; // FIXME: Avoid const_cast, make moveDir mutable in some other way? const_cast(hero)->moveDir = dirLookup[posOffset.y][posOffset.x]; } } void CPlayerInterface::heroKilled(const CGHeroInstance* hero) { EVENT_HANDLER_CALLED_BY_CLIENT; LOG_TRACE_PARAMS(logGlobal, "Hero %s killed handler for player %s", hero->name % playerID); const CArmedInstance *newSelection = nullptr; if (makingTurn) { //find new object for selection: either hero int next = adventureInt->getNextHeroIndex(vstd::find_pos(wanderingHeroes, hero)); if (next >= 0) newSelection = wanderingHeroes[next]; //or town if (!newSelection || newSelection == hero) { if (towns.empty()) newSelection = nullptr; else newSelection = towns.front(); } } wanderingHeroes -= hero; if (vstd::contains(paths, hero)) paths.erase(hero); adventureInt->heroList.update(hero); if (makingTurn && newSelection) adventureInt->select(newSelection, true); else if (adventureInt->selection == hero) adventureInt->selection = nullptr; } void CPlayerInterface::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) { EVENT_HANDLER_CALLED_BY_CLIENT; if(start && visitedObj) { if(visitedObj->getVisitSound()) CCS->soundh->playSound(visitedObj->getVisitSound().get()); } } void CPlayerInterface::heroCreated(const CGHeroInstance * hero) { EVENT_HANDLER_CALLED_BY_CLIENT; wanderingHeroes.push_back(hero); adventureInt->heroList.update(hero); } void CPlayerInterface::openTownWindow(const CGTownInstance * town) { if(castleInt) castleInt->close(); castleInt = nullptr; auto newCastleInt = std::make_shared(town); GH.pushInt(newCastleInt); } int3 CPlayerInterface::repairScreenPos(int3 pos) { if (pos.x<-CGI->mh->frameW) pos.x = -CGI->mh->frameW; if (pos.y<-CGI->mh->frameH) pos.y = -CGI->mh->frameH; if (pos.x>CGI->mh->sizes.x - adventureInt->terrain.tilesw + CGI->mh->frameW) pos.x = CGI->mh->sizes.x - adventureInt->terrain.tilesw + CGI->mh->frameW; if (pos.y>CGI->mh->sizes.y - adventureInt->terrain.tilesh + CGI->mh->frameH) pos.y = CGI->mh->sizes.y - adventureInt->terrain.tilesh + CGI->mh->frameH; return pos; } void CPlayerInterface::activateForSpectator() { adventureInt->state = CAdvMapInt::INGAME; adventureInt->activate(); adventureInt->minimap.activate(); } void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) { EVENT_HANDLER_CALLED_BY_CLIENT; if (which == 4) { if (CAltarWindow *ctw = dynamic_cast(GH.topInt().get())) ctw->setExpToLevel(); } else if (which < GameConstants::PRIMARY_SKILLS) //no need to redraw infowin if this is experience (exp is treated as prim skill with id==4) updateInfo(hero); } void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) { EVENT_HANDLER_CALLED_BY_CLIENT; CUniversityWindow* cuw = dynamic_cast(GH.topInt().get()); if (cuw) //university window is open { GH.totalRedraw(); } } void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero) { EVENT_HANDLER_CALLED_BY_CLIENT; updateInfo(hero); if (makingTurn && hero->tempOwner == playerID) adventureInt->heroList.update(hero); } void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero) { EVENT_HANDLER_CALLED_BY_CLIENT; if (makingTurn && hero->tempOwner == playerID) adventureInt->heroList.update(hero); } void CPlayerInterface::receivedResource() { EVENT_HANDLER_CALLED_BY_CLIENT; if (CMarketplaceWindow *mw = dynamic_cast(GH.topInt().get())) mw->resourceChanged(); GH.totalRedraw(); } void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector& skills, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); CCS->soundh->playSound(soundBase::heroNewLevel); GH.pushIntT(hero, pskill, skills, [=](ui32 selection) { cb->selectionMade(selection, queryID); }); } void CPlayerInterface::commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); CCS->soundh->playSound(soundBase::heroNewLevel); GH.pushIntT(commander, skills, [=](ui32 selection) { cb->selectionMade(selection, queryID); }); } void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) { EVENT_HANDLER_CALLED_BY_CLIENT; updateInfo(town); if (town->garrisonHero) //wandering hero moved to the garrison { CGI->mh->hideObject(town->garrisonHero); if (town->garrisonHero->tempOwner == playerID && vstd::contains(wanderingHeroes,town->garrisonHero)) // our hero wanderingHeroes -= town->garrisonHero; } if (town->visitingHero) //hero leaves garrison { CGI->mh->printObject(town->visitingHero); if (town->visitingHero->tempOwner == playerID && !vstd::contains(wanderingHeroes,town->visitingHero)) // our hero wanderingHeroes.push_back(town->visitingHero); } adventureInt->heroList.update(); adventureInt->updateNextHero(nullptr); if(castleInt) { castleInt->garr->selectSlot(nullptr); castleInt->garr->setArmy(town->getUpperArmy(), 0); castleInt->garr->setArmy(town->visitingHero, 1); castleInt->garr->recreateSlots(); castleInt->heroes->update(); } for (auto isa : GH.listInt) { CKingdomInterface *ki = dynamic_cast(isa.get()); if (ki) { ki->townChanged(town); ki->updateGarrisons(); } } GH.totalRedraw(); } void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) { EVENT_HANDLER_CALLED_BY_CLIENT; if (hero->tempOwner != playerID ) return; waitWhileDialog(); openTownWindow(town); } void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) { std::vector instances; if(auto obj = cb->getObj(id1)) instances.push_back(obj); if(id2 != ObjectInstanceID() && id2 != id1) { if(auto obj = cb->getObj(id2)) instances.push_back(obj); } garrisonsChanged(instances); } void CPlayerInterface::garrisonsChanged(std::vector objs) { boost::unique_lock un(*pim); for (auto object : objs) updateInfo(object); for (auto & elem : GH.listInt) { CGarrisonHolder *cgh = dynamic_cast(elem.get()); if (cgh) cgh->updateGarrisons(); if (CTradeWindow *cmw = dynamic_cast(elem.get())) { if (vstd::contains(objs, cmw->hero)) cmw->garrisonChanged(); } } GH.totalRedraw(); } void CPlayerInterface::garrisonChanged( const CGObjectInstance * obj) { garrisonsChanged(std::vector(1, obj)); } void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished { EVENT_HANDLER_CALLED_BY_CLIENT; switch (buildingID) { case BuildingID::FORT: case BuildingID::CITADEL: case BuildingID::CASTLE: case BuildingID::VILLAGE_HALL: case BuildingID::TOWN_HALL: case BuildingID::CITY_HALL: case BuildingID::CAPITOL: case BuildingID::RESOURCE_SILO: updateInfo(town); break; } if (castleInt) { castleInt->townlist->update(town); if (castleInt->town == town) { switch(what) { case 1: CCS->soundh->playSound(soundBase::newBuilding); castleInt->addBuilding(buildingID); break; case 2: castleInt->removeBuilding(buildingID); break; } } } adventureInt->townList.update(town); } void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) { //Don't wait for dialogs when we are non-active hot-seat player if (LOCPLINT == this) waitForAllDialogs(); } void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) { EVENT_HANDLER_CALLED_BY_CLIENT; if (settings["adventure"]["quickCombat"].Bool()) { autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); autofightingAI->init(cb); autofightingAI->battleStart(army1, army2, int3(0,0,0), hero1, hero2, side); isAutoFightOn = true; cb->registerBattleInterface(autofightingAI); // Player shouldn't be able to move on adventure map if quick combat is going adventureInt->quickCombatLock(); } //Don't wait for dialogs when we are non-active hot-seat player if (LOCPLINT == this) waitForAllDialogs(); BATTLE_EVENT_POSSIBLE_RETURN; } void CPlayerInterface::battleUnitsChanged(const std::vector & units, const std::vector & customEffects, const std::vector & battleLog) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; for(auto & info : units) { switch(info.operation) { case UnitChanges::EOperation::RESET_STATE: { const battle::Unit * unit = cb->battleGetUnitByID(info.id); if(!unit) { logGlobal->error("Invalid unit ID %d", info.id); continue; } auto iter = battleInt->creAnims.find(info.id); if(iter == battleInt->creAnims.end()) { logGlobal->error("Unit %d have no animation", info.id); continue; } auto animation = iter->second; if(unit->alive() && animation->isDead()) animation->setType(CCreatureAnim::HOLDING); //TODO: handle more cases } break; case UnitChanges::EOperation::REMOVE: battleInt->stackRemoved(info.id); break; case UnitChanges::EOperation::ADD: { const CStack * unit = cb->battleGetStackByID(info.id); if(!unit) { logGlobal->error("Invalid unit ID %d", info.id); continue; } battleInt->unitAdded(unit); } break; default: logGlobal->error("Unknown unit operation %d", (int)info.operation); break; } } battleInt->displayCustomEffects(customEffects); battleInt->displayBattleLog(battleLog); } void CPlayerInterface::battleObstaclesChanged(const std::vector & obstacles) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; bool needUpdate = false; for(auto & change : obstacles) { if(change.operation == BattleChanges::EOperation::ADD) { auto instance = cb->battleGetObstacleByID(change.id); if(instance) battleInt->obstaclePlaced(*instance); else logNetwork->error("Invalid obstacle instance %d", change.id); } else { needUpdate = true; } } if(needUpdate) //update accessible hexes battleInt->redrawBackgroundWithHexes(battleInt->activeStack); } void CPlayerInterface::battleCatapultAttacked(const CatapultAttack & ca) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->stackIsCatapulting(ca); } void CPlayerInterface::battleNewRound(int round) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->newRound(round); } void CPlayerInterface::actionStarted(const BattleAction &action) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; curAction = new BattleAction(action); battleInt->startAction(curAction); } void CPlayerInterface::actionFinished(const BattleAction &action) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->endAction(curAction); delete curAction; curAction = nullptr; } BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when it's turn of that stack { THREAD_CREATED_BY_CLIENT; logGlobal->trace("Awaiting command for %s", stack->nodeName()); auto stackId = stack->ID; auto stackName = stack->nodeName(); if (autofightingAI) { if (isAutoFightOn) { auto ret = autofightingAI->activeStack(stack); if (isAutoFightOn) { return ret; } } cb->unregisterBattleInterface(autofightingAI); autofightingAI.reset(); } CBattleInterface *b = battleInt; if(CBattleInterface::givenCommand.get()) { logGlobal->error("Command buffer must be clean! (we don't want to use old command)"); vstd::clear_pointer(CBattleInterface::givenCommand.data); } { boost::unique_lock un(*pim); b->stackActivated(stack); //Regeneration & mana drain go there } //wait till BattleInterface sets its command boost::unique_lock lock(CBattleInterface::givenCommand.mx); while(!CBattleInterface::givenCommand.data) { CBattleInterface::givenCommand.cond.wait(lock); if (!battleInt) //battle ended while we were waiting for movement (eg. because of spell) throw boost::thread_interrupted(); //will shut the thread peacefully } //tidy up BattleAction ret = *(CBattleInterface::givenCommand.data); vstd::clear_pointer(CBattleInterface::givenCommand.data); if(ret.actionType == EActionType::CANCEL) { if(stackId != ret.stackNumber) logGlobal->error("Not current active stack action canceled"); logGlobal->trace("Canceled command for %s", stackName); } else logGlobal->trace("Giving command for %s", stackName); return ret; } void CPlayerInterface::battleEnd(const BattleResult *br) { EVENT_HANDLER_CALLED_BY_CLIENT; if (isAutoFightOn) { isAutoFightOn = false; cb->unregisterBattleInterface(autofightingAI); autofightingAI.reset(); adventureInt->quickCombatUnlock(); if (!battleInt) { GH.pushIntT(*br, *this); // #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it. // Otherwise NewTurn causes freeze. waitWhileDialog(); return; } } BATTLE_EVENT_POSSIBLE_RETURN; battleInt->battleFinished(*br); } void CPlayerInterface::battleStackMoved(const CStack * stack, std::vector dest, int distance) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->stackMoved(stack, dest, distance); } void CPlayerInterface::battleSpellCast( const BattleSpellCast *sc ) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->spellCast(sc); } void CPlayerInterface::battleStacksEffectsSet( const SetStackEffect & sse ) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->battleStacksEffectsSet(sse); } void CPlayerInterface::battleTriggerEffect (const BattleTriggerEffect & bte) { EVENT_HANDLER_CALLED_BY_CLIENT; //TODO why is this different (no return on LOPLINT != this) ? RETURN_IF_QUICK_COMBAT; battleInt->battleTriggerEffect(bte); } void CPlayerInterface::battleStacksAttacked(const std::vector & bsa, const std::vector & battleLog) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; std::vector arg; for(auto & elem : bsa) { const CStack * defender = cb->battleGetStackByID(elem.stackAttacked, false); const CStack * attacker = cb->battleGetStackByID(elem.attackerID, false); if(elem.isEffect()) { if(defender && !elem.isSecondary()) battleInt->displayEffect(elem.effect, defender->getPosition()); } if(elem.isSpell()) { if(defender) battleInt->displaySpellEffect(elem.spellID, defender->getPosition()); } //FIXME: why action is deleted during enchanter cast? bool remoteAttack = false; if(LOCPLINT->curAction) remoteAttack |= LOCPLINT->curAction->actionType != EActionType::WALK_AND_ATTACK; StackAttackedInfo to_put = {defender, elem.damageAmount, elem.killedAmount, attacker, remoteAttack, elem.killed(), elem.willRebirth(), elem.cloneKilled()}; arg.push_back(to_put); } battleInt->stacksAreAttacked(arg, battleLog); } void CPlayerInterface::battleAttack(const BattleAttack * ba) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; assert(curAction); const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking); if(!attacker) { logGlobal->error("Attacking stack not found"); return; } if(ba->lucky()) //lucky hit { battleInt->console->addText(attacker->formatGeneralMessage(-45)); battleInt->displayEffect(18, attacker->getPosition()); CCS->soundh->playSound(soundBase::GOODLUCK); } if(ba->unlucky()) //unlucky hit { battleInt->console->addText(attacker->formatGeneralMessage(-44)); battleInt->displayEffect(48, attacker->getPosition()); CCS->soundh->playSound(soundBase::BADLUCK); } if(ba->deathBlow()) { battleInt->console->addText(attacker->formatGeneralMessage(365)); for(auto & elem : ba->bsa) { const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked); battleInt->displayEffect(73, attacked->getPosition()); } CCS->soundh->playSound(soundBase::deathBlow); } battleInt->displayCustomEffects(ba->customEffects); battleInt->waitForAnims(); auto actionTarget = curAction->getTarget(cb.get()); if(actionTarget.empty() || (actionTarget.size() < 2 && !ba->shot())) { logNetwork->error("Invalid current action: no destination."); return; } if(ba->shot()) { for(auto & elem : ba->bsa) { if(!elem.isSecondary()) //display projectile only for primary target { const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked); battleInt->stackAttacking(attacker, attacked->getPosition(), attacked, true); } } } else { auto attackTarget = actionTarget.at(1).hexValue; //TODO: use information from BattleAttack but not curAction int shift = 0; if(ba->counter() && BattleHex::mutualPosition(attackTarget, attacker->getPosition()) < 0) { int distp = BattleHex::getDistance(attackTarget + 1, attacker->getPosition()); int distm = BattleHex::getDistance(attackTarget - 1, attacker->getPosition()); if(distp < distm) shift = 1; else shift = -1; } const CStack * attacked = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked); battleInt->stackAttacking(attacker, ba->counter() ? BattleHex(attackTarget + shift) : attackTarget, attacked, false); } //battleInt->waitForAnims(); //FIXME: freeze if(ba->spellLike()) { //TODO: use information from BattleAttack but not curAction auto destination = actionTarget.at(0).hexValue; //display hit animation SpellID spellID = ba->spellID; battleInt->displaySpellHit(spellID, destination); } } void CPlayerInterface::battleGateStateChanged(const EGateState state) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->gateStateChanged(state); } void CPlayerInterface::yourTacticPhase(int distance) { THREAD_CREATED_BY_CLIENT; while(battleInt && battleInt->tacticsMode) boost::this_thread::sleep(boost::posix_time::millisec(1)); } void CPlayerInterface::showComp(const Component &comp, std::string message) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); //Fix for mantis #98 CCS->soundh->playSoundFromSet(CCS->soundh->pickupSounds); adventureInt->infoBar.showComponent(comp, message); } void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector & components, int soundID) { EVENT_HANDLER_CALLED_BY_CLIENT; if (settings["session"]["autoSkip"].Bool() && !LOCPLINT->shiftPressed()) { return; } std::vector> intComps; for (auto & component : components) intComps.push_back(std::make_shared(component)); showInfoDialog(text,intComps,soundID); } void CPlayerInterface::showInfoDialog(const std::string & text, std::shared_ptr component) { std::vector> intComps; intComps.push_back(component); showInfoDialog(text, intComps, soundBase::sound_todo); } void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector> & components, int soundID) { LOG_TRACE_PARAMS(logGlobal, "player=%s, text=%s, is LOCPLINT=%d", playerID % text % (this==LOCPLINT)); waitWhileDialog(); if (settings["session"]["autoSkip"].Bool() && !LOCPLINT->shiftPressed()) { return; } std::shared_ptr temp = CInfoWindow::create(text, playerID, components); if (makingTurn && GH.listInt.size() && LOCPLINT == this) { CCS->soundh->playSound(static_cast(soundID)); showingDialog->set(true); stopMovement(); // interrupt movement to show dialog GH.pushInt(temp); } else { dialogs.push_back(temp); } } void CPlayerInterface::showInfoDialogAndWait(std::vector & components, const MetaString & text) { EVENT_HANDLER_CALLED_BY_CLIENT; std::string str; text.toString(str); showInfoDialog(str, components, 0); waitWhileDialog(); } void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components) { boost::unique_lock un(*pim); stopMovement(); LOCPLINT->showingDialog->setn(true); CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID); } void CPlayerInterface::showBlockingDialog( const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel ) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); stopMovement(); CCS->soundh->playSound(static_cast(soundID)); if (!selection && cancel) //simple yes/no dialog { std::vector> intComps; for (auto & component : components) intComps.push_back(std::make_shared(component)); //will be deleted by close in window showYesNoDialog(text, [=](){ cb->selectionMade(1, askID); }, [=](){ cb->selectionMade(0, askID); }, intComps); } else if (selection) { std::vector> intComps; for (auto & component : components) intComps.push_back(std::make_shared(component)); //will be deleted by CSelWindow::close std::vector > > pom; pom.push_back(std::pair >("IOKAY.DEF",0)); if (cancel) { pom.push_back(std::pair >("ICANCEL.DEF",0)); } int charperline = 35; if (pom.size() > 1) charperline = 50; GH.pushIntT(text, playerID, charperline, intComps, pom, askID); intComps[0]->clickLeft(true, false); } } void CPlayerInterface::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { EVENT_HANDLER_CALLED_BY_CLIENT; int choosenExit = -1; auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); if (destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) choosenExit = vstd::find_pos(exits, neededExit); cb->selectionMade(choosenExit, askID); } void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) { EVENT_HANDLER_CALLED_BY_CLIENT; auto selectCallback = [=](int selection) { JsonNode reply(JsonNode::JsonType::DATA_INTEGER); reply.Integer() = selection; cb->sendQueryReply(reply, askID); }; auto cancelCallback = [=]() { JsonNode reply(JsonNode::JsonType::DATA_NULL); cb->sendQueryReply(reply, askID); }; const std::string localTitle = title.toString(); const std::string localDescription = description.toString(); std::vector tempList; tempList.reserve(objects.size()); for(auto item : objects) tempList.push_back(item.getNum()); CComponent localIconC(icon); std::shared_ptr localIcon = localIconC.image; localIconC.removeChild(localIcon.get(), false); std::shared_ptr wnd = std::make_shared(tempList, localIcon, localTitle, localDescription, selectCallback); wnd->onExit = cancelCallback; GH.pushInt(wnd); } void CPlayerInterface::tileRevealed(const std::unordered_set &pos) { EVENT_HANDLER_CALLED_BY_CLIENT; //FIXME: wait for dialog? Magi hut/eye would benefit from this but may break other areas for (auto & po : pos) adventureInt->minimap.showTile(po); if (!pos.empty()) GH.totalRedraw(); } void CPlayerInterface::tileHidden(const std::unordered_set &pos) { EVENT_HANDLER_CALLED_BY_CLIENT; for (auto & po : pos) adventureInt->minimap.hideTile(po); if (!pos.empty()) GH.totalRedraw(); } void CPlayerInterface::openHeroWindow(const CGHeroInstance *hero) { boost::unique_lock un(*pim); GH.pushIntT(hero); } void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town ) { EVENT_HANDLER_CALLED_BY_CLIENT; if (const CGTownInstance * townObj = dynamic_cast(town)) { CFortScreen *fs = dynamic_cast(GH.topInt().get()); if (fs) fs->creaturesChanged(); for(auto isa : GH.listInt) { CKingdomInterface *ki = dynamic_cast(isa.get()); if (ki && townObj) ki->townChanged(townObj); } } else if (GH.listInt.size() && (town->ID == Obj::CREATURE_GENERATOR1 || town->ID == Obj::CREATURE_GENERATOR4 || town->ID == Obj::WAR_MACHINE_FACTORY)) { CRecruitmentWindow *crw = dynamic_cast(GH.topInt().get()); if (crw && crw->dwelling == town) crw->availableCreaturesChanged(); } } void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus &bonus, bool gain ) { EVENT_HANDLER_CALLED_BY_CLIENT; if (bonus.type == Bonus::NONE) return; updateInfo(hero); if ((bonus.type == Bonus::FLYING_MOVEMENT || bonus.type == Bonus::WATER_WALKING) && !gain) { //recalculate paths because hero has lost bonus influencing pathfinding eraseCurrentPathOf(hero, false); } } template void CPlayerInterface::serializeTempl( Handler &h, const int version ) { if(version < 774 && !h.saving) { bool observerInDuelMode = false; h & observerInDuelMode; } h & wanderingHeroes; h & towns; h & sleepingHeroes; std::map pathsMap; //hero -> dest if (h.saving) { for (auto &p : paths) { if (p.second.nodes.size()) pathsMap[p.first] = p.second.endPos(); else logGlobal->debug("%s has assigned an empty path! Ignoring it...", p.first->name); } h & pathsMap; } else { h & pathsMap; if (cb) for (auto &p : pathsMap) { CGPath path; cb->getPathsInfo(p.first)->getPath(path, p.second); paths[p.first] = path; logGlobal->trace("Restored path for hero %s leading to %s with %d nodes", p.first->nodeName(), p.second.toString(), path.nodes.size()); } } h & spellbookSettings; } void CPlayerInterface::saveGame( BinarySerializer & h, const int version ) { EVENT_HANDLER_CALLED_BY_CLIENT; serializeTempl(h,version); } void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version ) { EVENT_HANDLER_CALLED_BY_CLIENT; serializeTempl(h,version); firstCall = -1; } void CPlayerInterface::moveHero( const CGHeroInstance *h, CGPath path ) { LOG_TRACE(logGlobal); if (!LOCPLINT->makingTurn) return; if (!h) return; //can't find hero //It shouldn't be possible to move hero with open dialog (or dialog waiting in bg) if (showingDialog->get() || !dialogs.empty()) return; setMovementStatus(true); if (adventureInt && adventureInt->isHeroSleeping(h)) { adventureInt->sleepWake->clickLeft(true, false); adventureInt->sleepWake->clickLeft(false, true); //could've just called //adventureInt->fsleepWake(); //but no authentic button click/sound ;-) } boost::thread moveHeroTask(std::bind(&CPlayerInterface::doMoveHero,this,h,path)); } bool CPlayerInterface::shiftPressed() const { return isShiftKeyDown(); } bool CPlayerInterface::altPressed() const { return isAltKeyDown(); } void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; auto onEnd = [=](){ cb->selectionMade(0, queryID); }; if (stillMoveHero.get() == DURING_MOVE && adventureInt->terrain.currentPath && adventureInt->terrain.currentPath->nodes.size() > 1) //to ignore calls on passing through garrisons { onEnd(); return; } waitForAllDialogs(); auto cgw = std::make_shared(up, down, removableUnits); cgw->quit->addCallback(onEnd); GH.pushInt(cgw); } /** * Shows the dialog that appears when right-clicking an artifact that can be assembled * into a combinational one on an artifact screen. Does not require the combination of * artifacts to be legal. * @param artifactID ID of a constituent artifact. * @param assembleTo ID of artifact to assemble a constituent into, not used when assemble * is false. * @param assemble True if the artifact is to be assembled, false if it is to be disassembled. */ void CPlayerInterface::showArtifactAssemblyDialog (ui32 artifactID, ui32 assembleTo, bool assemble, CFunctionList onYes, CFunctionList onNo) { const CArtifact &artifact = *CGI->arth->artifacts[artifactID]; std::string text = artifact.Description(); text += "\n\n"; std::vector> scs; if(assemble) { const CArtifact &assembledArtifact = *CGI->arth->artifacts[assembleTo]; // You possess all of the components to... text += boost::str(boost::format(CGI->generaltexth->allTexts[732]) % assembledArtifact.Name()); // Picture of assembled artifact at bottom. auto sc = std::make_shared(CComponent::artifact, assembledArtifact.id, 0); //sc->description = assembledArtifact.Description(); //sc->subtitle = assembledArtifact.Name(); scs.push_back(sc); } else { // Do you wish to disassemble this artifact? text += CGI->generaltexth->allTexts[733]; } showYesNoDialog(text, onYes, onNo, scs); } void CPlayerInterface::requestRealized( PackageApplied *pa ) { EVENT_HANDLER_CALLED_BY_CLIENT; if (pa->packType == typeList.getTypeID() && stillMoveHero.get() == DURING_MOVE && destinationTeleport == ObjectInstanceID()) stillMoveHero.setn(CONTINUE_MOVE); if (destinationTeleport != ObjectInstanceID() && pa->packType == typeList.getTypeID() && stillMoveHero.get() == DURING_MOVE) { // After teleportation via CGTeleport object is finished destinationTeleport = ObjectInstanceID(); destinationTeleportPos = int3(-1); stillMoveHero.setn(CONTINUE_MOVE); } } void CPlayerInterface::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) { EVENT_HANDLER_CALLED_BY_CLIENT; GH.pushIntT(hero1, hero2, query); } void CPlayerInterface::objectPropertyChanged(const SetObjectProperty * sop) { EVENT_HANDLER_CALLED_BY_CLIENT; //redraw minimap if owner changed if (sop->what == ObjProperty::OWNER) { const CGObjectInstance * obj = cb->getObj(sop->id); std::set pos = obj->getBlockedPos(); for (auto & po : pos) { if (cb->isVisible(po)) adventureInt->minimap.showTile(po); } if (obj->ID == Obj::TOWN) { if (obj->tempOwner == playerID) towns.push_back(static_cast(obj)); else towns -= obj; adventureInt->townList.update(); } assert(cb->getTownsInfo().size() == towns.size()); } } void CPlayerInterface::initializeHeroTownList() { if(!wanderingHeroes.size()) { std::vector heroes = cb->getHeroesInfo(); for(auto & hero : heroes) { if(!hero->inTownGarrison) wanderingHeroes.push_back(hero); } } if(!towns.size()) towns = cb->getTownsInfo(); if(adventureInt) adventureInt->updateNextHero(nullptr); } void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); auto recruitCb = [=](CreatureID id, int count) { LOCPLINT->cb->recruitCreatures(dwelling, dst, id, count, -1); }; GH.pushIntT(dwelling, level, dst, recruitCb); } void CPlayerInterface::waitWhileDialog(bool unlockPim) { if (GH.amIGuiThread()) { logGlobal->warn("Cannot wait for dialogs in gui thread (deadlock risk)!"); return; } auto unlock = vstd::makeUnlockGuardIf(*pim, unlockPim); boost::unique_lock un(showingDialog->mx); while(showingDialog->data) showingDialog->cond.wait(un); } void CPlayerInterface::showShipyardDialog(const IShipyard *obj) { EVENT_HANDLER_CALLED_BY_CLIENT; auto state = obj->shipyardStatus(); std::vector cost; obj->getBoatCost(cost); GH.pushIntT(cost, state, obj->getBoatType(), [=](){ cb->buildBoat(obj); }); } void CPlayerInterface::newObject( const CGObjectInstance * obj ) { EVENT_HANDLER_CALLED_BY_CLIENT; //we might have built a boat in shipyard in opened town screen if (obj->ID == Obj::BOAT && LOCPLINT->castleInt && obj->pos-obj->getVisitableOffset() == LOCPLINT->castleInt->town->bestLocation()) { CCS->soundh->playSound(soundBase::newBuilding); LOCPLINT->castleInt->addBuilding(BuildingID::SHIP); } } void CPlayerInterface::centerView (int3 pos, int focusTime) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); CCS->curh->hide(); adventureInt->centerOn (pos); if (focusTime) { GH.totalRedraw(); { auto unlockPim = vstd::makeUnlockGuard(*pim); IgnoreEvents ignore(*this); SDL_Delay(focusTime); } } CCS->curh->show(); } void CPlayerInterface::objectRemoved(const CGObjectInstance * obj) { EVENT_HANDLER_CALLED_BY_CLIENT; if(LOCPLINT->cb->getCurrentPlayer() == playerID && obj->getRemovalSound()) { waitWhileDialog(); CCS->soundh->playSound(obj->getRemovalSound().get()); } if(obj->ID == Obj::HERO && obj->tempOwner == playerID) { const CGHeroInstance * h = static_cast(obj); heroKilled(h); } } bool CPlayerInterface::ctrlPressed() const { return isCtrlKeyDown(); } const CArmedInstance * CPlayerInterface::getSelection() { return currentSelection; } void CPlayerInterface::setSelection(const CArmedInstance * obj) { currentSelection = obj; updateAmbientSounds(true); } void CPlayerInterface::update() { // Make sure that gamestate won't change when GUI objects may obtain its parts on event processing or drawing request boost::shared_lock gsLock(CGameState::mutex); // While mutexes were locked away we may be have stopped being the active interface if (LOCPLINT != this) return; //if there are any waiting dialogs, show them if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get()) { showingDialog->set(true); GH.pushInt(dialogs.front()); dialogs.pop_front(); } //in some conditions we may receive calls before selection is initialized - we must ignore them if(adventureInt && GH.topInt() == adventureInt && (!adventureInt->selection && !settings["session"]["spectate"].Bool())) { return; } // Handles mouse and key input GH.updateTime(); GH.handleEvents(); if (!adventureInt || adventureInt->isActive()) GH.simpleRedraw(); else if((adventureInt->swipeEnabled && adventureInt->swipeMovementRequested) || adventureInt->scrollingDir) GH.totalRedraw(); //player forces map scrolling though interface is disabled else GH.simpleRedraw(); } int CPlayerInterface::getLastIndex( std::string namePrefix) { using namespace boost::filesystem; using namespace boost::algorithm; path gamesDir = VCMIDirs::get().userSavePath(); std::map dates; //save number => datestamp const directory_iterator enddir; if (!exists(gamesDir)) create_directory(gamesDir); else for (directory_iterator dir(gamesDir); dir != enddir; ++dir) { if (is_regular(dir->status())) { std::string name = dir->path().filename().string(); if (starts_with(name, namePrefix) && ends_with(name, ".vcgm1")) { char nr = name[namePrefix.size()]; if (std::isdigit(nr)) dates[last_write_time(dir->path())] = boost::lexical_cast(nr); } } } if (!dates.empty()) return (--dates.end())->second; //return latest file number return 0; } void CPlayerInterface::initMovement( const TryMoveHero &details, const CGHeroInstance * ho, const int3 &hp ) { if (details.end.x+1 == details.start.x && details.end.y+1 == details.start.y) //tl { //ho->moveDir = 1; ho->isStanding = false; CGI->mh->ttiles[hp.x-3][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -31, -31))); CGI->mh->ttiles[hp.x-2][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 1, -31))); CGI->mh->ttiles[hp.x-1][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 33, -31))); CGI->mh->ttiles[hp.x][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 65, -31))); CGI->mh->ttiles[hp.x-3][hp.y-1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -31, 1))); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 1, 1), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 33, 1), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 65, 1), ho->id); CGI->mh->ttiles[hp.x-3][hp.y][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -31, 33))); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 1, 33), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 33, 33), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 65, 33), ho->id); std::stable_sort(CGI->mh->ttiles[hp.x-3][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-3][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-2][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-2][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-1][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-1][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-3][hp.y-1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-3][hp.y-1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-3][hp.y][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-3][hp.y][hp.z].objects.end(), objectBlitOrderSorter); } else if (details.end.x == details.start.x && details.end.y+1 == details.start.y) //t { //ho->moveDir = 2; ho->isStanding = false; CGI->mh->ttiles[hp.x-2][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 0, -31))); CGI->mh->ttiles[hp.x-1][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 32, -31))); CGI->mh->ttiles[hp.x][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 64, -31))); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 0, 1), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 32, 1), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 64, 1), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 0, 33), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 32, 33), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 64, 33), ho->id); std::stable_sort(CGI->mh->ttiles[hp.x-2][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-2][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-1][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-1][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); } else if (details.end.x-1 == details.start.x && details.end.y+1 == details.start.y) //tr { //ho->moveDir = 3; ho->isStanding = false; CGI->mh->ttiles[hp.x-2][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -1, -31))); CGI->mh->ttiles[hp.x-1][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 31, -31))); CGI->mh->ttiles[hp.x][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 63, -31))); CGI->mh->ttiles[hp.x+1][hp.y-2][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 95, -31))); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, -1, 1), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 31, 1), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 63, 1), ho->id); CGI->mh->ttiles[hp.x+1][hp.y-1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 95, 1))); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, -1, 33), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 31, 33), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 63, 33), ho->id); CGI->mh->ttiles[hp.x+1][hp.y][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 95, 33))); std::stable_sort(CGI->mh->ttiles[hp.x-2][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-2][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-1][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-1][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x+1][hp.y-2][hp.z].objects.begin(), CGI->mh->ttiles[hp.x+1][hp.y-2][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x+1][hp.y-1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x+1][hp.y-1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x+1][hp.y][hp.z].objects.begin(), CGI->mh->ttiles[hp.x+1][hp.y][hp.z].objects.end(), objectBlitOrderSorter); } else if (details.end.x-1 == details.start.x && details.end.y == details.start.y) //r { //ho->moveDir = 4; ho->isStanding = false; subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, -1, 0), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 31, 0), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 63, 0), ho->id); CGI->mh->ttiles[hp.x+1][hp.y-1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 95, 0))); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, -1, 32), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 31, 32), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 63, 32), ho->id); CGI->mh->ttiles[hp.x+1][hp.y][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 95, 32))); std::stable_sort(CGI->mh->ttiles[hp.x+1][hp.y-1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x+1][hp.y-1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x+1][hp.y][hp.z].objects.begin(), CGI->mh->ttiles[hp.x+1][hp.y][hp.z].objects.end(), objectBlitOrderSorter); } else if (details.end.x-1 == details.start.x && details.end.y-1 == details.start.y) //br { //ho->moveDir = 5; ho->isStanding = false; subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, -1, -1), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 31, -1), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 63, -1), ho->id); CGI->mh->ttiles[hp.x+1][hp.y-1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 95, -1))); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, -1, 31), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 31, 31), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 63, 31), ho->id); CGI->mh->ttiles[hp.x+1][hp.y][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 95, 31))); CGI->mh->ttiles[hp.x-2][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -1, 63))); CGI->mh->ttiles[hp.x-1][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 31, 63))); CGI->mh->ttiles[hp.x][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 63, 63))); CGI->mh->ttiles[hp.x+1][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 95, 63))); std::stable_sort(CGI->mh->ttiles[hp.x+1][hp.y-1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x+1][hp.y-1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x+1][hp.y][hp.z].objects.begin(), CGI->mh->ttiles[hp.x+1][hp.y][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-2][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-2][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-1][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-1][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x+1][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x+1][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); } else if (details.end.x == details.start.x && details.end.y-1 == details.start.y) //b { //ho->moveDir = 6; ho->isStanding = false; subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 0, -1), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 32, -1), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 64, -1), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 0, 31), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 32, 31), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 64, 31), ho->id); CGI->mh->ttiles[hp.x-2][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 0, 63))); CGI->mh->ttiles[hp.x-1][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 32, 63))); CGI->mh->ttiles[hp.x][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 64, 63))); std::stable_sort(CGI->mh->ttiles[hp.x-2][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-2][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-1][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-1][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); } else if (details.end.x+1 == details.start.x && details.end.y-1 == details.start.y) //bl { //ho->moveDir = 7; ho->isStanding = false; CGI->mh->ttiles[hp.x-3][hp.y-1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -31, -1))); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 1, -1), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 33, -1), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 65, -1), ho->id); CGI->mh->ttiles[hp.x-3][hp.y][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -31, 31))); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 1, 31), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 33, 31), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 65, 31), ho->id); CGI->mh->ttiles[hp.x-3][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -31, 63))); CGI->mh->ttiles[hp.x-2][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 1, 63))); CGI->mh->ttiles[hp.x-1][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 33, 63))); CGI->mh->ttiles[hp.x][hp.y+1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, 65, 63))); std::stable_sort(CGI->mh->ttiles[hp.x-3][hp.y-1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-3][hp.y-1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-3][hp.y][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-3][hp.y][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-3][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-3][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-2][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-2][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-1][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-1][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x][hp.y+1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x][hp.y+1][hp.z].objects.end(), objectBlitOrderSorter); } else if (details.end.x+1 == details.start.x && details.end.y == details.start.y) //l { //ho->moveDir = 8; ho->isStanding = false; CGI->mh->ttiles[hp.x-3][hp.y-1][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -31, 0))); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 1, 0), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 33, 0), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 65, 0), ho->id); CGI->mh->ttiles[hp.x-3][hp.y][hp.z].objects.push_back(TerrainTileObject(ho, genRect(32, 32, -31, 32))); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 1, 32), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 33, 32), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 65, 32), ho->id); std::stable_sort(CGI->mh->ttiles[hp.x-3][hp.y-1][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-3][hp.y-1][hp.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[hp.x-3][hp.y][hp.z].objects.begin(), CGI->mh->ttiles[hp.x-3][hp.y][hp.z].objects.end(), objectBlitOrderSorter); } } void CPlayerInterface::movementPxStep( const TryMoveHero &details, int i, const int3 &hp, const CGHeroInstance * ho ) { if (details.end.x+1 == details.start.x && details.end.y+1 == details.start.y) //tl { //setting advmap shift adventureInt->terrain.moveX = i-32; adventureInt->terrain.moveY = i-32; subRect(hp.x-3, hp.y-2, hp.z, genRect(32, 32, -31+i, -31+i), ho->id); subRect(hp.x-2, hp.y-2, hp.z, genRect(32, 32, 1+i, -31+i), ho->id); subRect(hp.x-1, hp.y-2, hp.z, genRect(32, 32, 33+i, -31+i), ho->id); subRect(hp.x, hp.y-2, hp.z, genRect(32, 32, 65+i, -31+i), ho->id); subRect(hp.x-3, hp.y-1, hp.z, genRect(32, 32, -31+i, 1+i), ho->id); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 1+i, 1+i), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 33+i, 1+i), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 65+i, 1+i), ho->id); subRect(hp.x-3, hp.y, hp.z, genRect(32, 32, -31+i, 33+i), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 1+i, 33+i), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 33+i, 33+i), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 65+i, 33+i), ho->id); } else if (details.end.x == details.start.x && details.end.y+1 == details.start.y) //t { //setting advmap shift adventureInt->terrain.moveY = i-32; subRect(hp.x-2, hp.y-2, hp.z, genRect(32, 32, 0, -31+i), ho->id); subRect(hp.x-1, hp.y-2, hp.z, genRect(32, 32, 32, -31+i), ho->id); subRect(hp.x, hp.y-2, hp.z, genRect(32, 32, 64, -31+i), ho->id); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 0, 1+i), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 32, 1+i), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 64, 1+i), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 0, 33+i), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 32, 33+i), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 64, 33+i), ho->id); } else if (details.end.x-1 == details.start.x && details.end.y+1 == details.start.y) //tr { //setting advmap shift adventureInt->terrain.moveX = -i+32; adventureInt->terrain.moveY = i-32; subRect(hp.x-2, hp.y-2, hp.z, genRect(32, 32, -1-i, -31+i), ho->id); subRect(hp.x-1, hp.y-2, hp.z, genRect(32, 32, 31-i, -31+i), ho->id); subRect(hp.x, hp.y-2, hp.z, genRect(32, 32, 63-i, -31+i), ho->id); subRect(hp.x+1, hp.y-2, hp.z, genRect(32, 32, 95-i, -31+i), ho->id); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, -1-i, 1+i), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 31-i, 1+i), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 63-i, 1+i), ho->id); subRect(hp.x+1, hp.y-1, hp.z, genRect(32, 32, 95-i, 1+i), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, -1-i, 33+i), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 31-i, 33+i), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 63-i, 33+i), ho->id); subRect(hp.x+1, hp.y, hp.z, genRect(32, 32, 95-i, 33+i), ho->id); } else if (details.end.x-1 == details.start.x && details.end.y == details.start.y) //r { //setting advmap shift adventureInt->terrain.moveX = -i+32; subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, -1-i, 0), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 31-i, 0), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 63-i, 0), ho->id); subRect(hp.x+1, hp.y-1, hp.z, genRect(32, 32, 95-i, 0), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, -1-i, 32), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 31-i, 32), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 63-i, 32), ho->id); subRect(hp.x+1, hp.y, hp.z, genRect(32, 32, 95-i, 32), ho->id); } else if (details.end.x-1 == details.start.x && details.end.y-1 == details.start.y) //br { //setting advmap shift adventureInt->terrain.moveX = -i+32; adventureInt->terrain.moveY = -i+32; subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, -1-i, -1-i), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 31-i, -1-i), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 63-i, -1-i), ho->id); subRect(hp.x+1, hp.y-1, hp.z, genRect(32, 32, 95-i, -1-i), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, -1-i, 31-i), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 31-i, 31-i), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 63-i, 31-i), ho->id); subRect(hp.x+1, hp.y, hp.z, genRect(32, 32, 95-i, 31-i), ho->id); subRect(hp.x-2, hp.y+1, hp.z, genRect(32, 32, -1-i, 63-i), ho->id); subRect(hp.x-1, hp.y+1, hp.z, genRect(32, 32, 31-i, 63-i), ho->id); subRect(hp.x, hp.y+1, hp.z, genRect(32, 32, 63-i, 63-i), ho->id); subRect(hp.x+1, hp.y+1, hp.z, genRect(32, 32, 95-i, 63-i), ho->id); } else if (details.end.x == details.start.x && details.end.y-1 == details.start.y) //b { //setting advmap shift adventureInt->terrain.moveY = -i+32; subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 0, -1-i), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 32, -1-i), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 64, -1-i), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 0, 31-i), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 32, 31-i), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 64, 31-i), ho->id); subRect(hp.x-2, hp.y+1, hp.z, genRect(32, 32, 0, 63-i), ho->id); subRect(hp.x-1, hp.y+1, hp.z, genRect(32, 32, 32, 63-i), ho->id); subRect(hp.x, hp.y+1, hp.z, genRect(32, 32, 64, 63-i), ho->id); } else if (details.end.x+1 == details.start.x && details.end.y-1 == details.start.y) //bl { //setting advmap shift adventureInt->terrain.moveX = i-32; adventureInt->terrain.moveY = -i+32; subRect(hp.x-3, hp.y-1, hp.z, genRect(32, 32, -31+i, -1-i), ho->id); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 1+i, -1-i), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 33+i, -1-i), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 65+i, -1-i), ho->id); subRect(hp.x-3, hp.y, hp.z, genRect(32, 32, -31+i, 31-i), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 1+i, 31-i), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 33+i, 31-i), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 65+i, 31-i), ho->id); subRect(hp.x-3, hp.y+1, hp.z, genRect(32, 32, -31+i, 63-i), ho->id); subRect(hp.x-2, hp.y+1, hp.z, genRect(32, 32, 1+i, 63-i), ho->id); subRect(hp.x-1, hp.y+1, hp.z, genRect(32, 32, 33+i, 63-i), ho->id); subRect(hp.x, hp.y+1, hp.z, genRect(32, 32, 65+i, 63-i), ho->id); } else if (details.end.x+1 == details.start.x && details.end.y == details.start.y) //l { //setting advmap shift adventureInt->terrain.moveX = i-32; subRect(hp.x-3, hp.y-1, hp.z, genRect(32, 32, -31+i, 0), ho->id); subRect(hp.x-2, hp.y-1, hp.z, genRect(32, 32, 1+i, 0), ho->id); subRect(hp.x-1, hp.y-1, hp.z, genRect(32, 32, 33+i, 0), ho->id); subRect(hp.x, hp.y-1, hp.z, genRect(32, 32, 65+i, 0), ho->id); subRect(hp.x-3, hp.y, hp.z, genRect(32, 32, -31+i, 32), ho->id); subRect(hp.x-2, hp.y, hp.z, genRect(32, 32, 1+i, 32), ho->id); subRect(hp.x-1, hp.y, hp.z, genRect(32, 32, 33+i, 32), ho->id); subRect(hp.x, hp.y, hp.z, genRect(32, 32, 65+i, 32), ho->id); } } void CPlayerInterface::finishMovement( const TryMoveHero &details, const int3 &hp, const CGHeroInstance * ho ) { adventureInt->terrain.moveX = adventureInt->terrain.moveY = 0; if (details.end.x+1 == details.start.x && details.end.y+1 == details.start.y) //tl { delObjRect(hp.x, hp.y-2, hp.z, ho->id); delObjRect(hp.x, hp.y-1, hp.z, ho->id); delObjRect(hp.x, hp.y, hp.z, ho->id); delObjRect(hp.x-1, hp.y, hp.z, ho->id); delObjRect(hp.x-2, hp.y, hp.z, ho->id); delObjRect(hp.x-3, hp.y, hp.z, ho->id); } else if (details.end.x == details.start.x && details.end.y+1 == details.start.y) //t { delObjRect(hp.x, hp.y, hp.z, ho->id); delObjRect(hp.x-1, hp.y, hp.z, ho->id); delObjRect(hp.x-2, hp.y, hp.z, ho->id); } else if (details.end.x-1 == details.start.x && details.end.y+1 == details.start.y) //tr { delObjRect(hp.x-2, hp.y-2, hp.z, ho->id); delObjRect(hp.x-2, hp.y-1, hp.z, ho->id); delObjRect(hp.x+1, hp.y, hp.z, ho->id); delObjRect(hp.x, hp.y, hp.z, ho->id); delObjRect(hp.x-1, hp.y, hp.z, ho->id); delObjRect(hp.x-2, hp.y, hp.z, ho->id); } else if (details.end.x-1 == details.start.x && details.end.y == details.start.y) //r { delObjRect(hp.x-2, hp.y-1, hp.z, ho->id); delObjRect(hp.x-2, hp.y, hp.z, ho->id); } else if (details.end.x-1 == details.start.x && details.end.y-1 == details.start.y) //br { delObjRect(hp.x-2, hp.y+1, hp.z, ho->id); delObjRect(hp.x-2, hp.y, hp.z, ho->id); delObjRect(hp.x+1, hp.y-1, hp.z, ho->id); delObjRect(hp.x, hp.y-1, hp.z, ho->id); delObjRect(hp.x-1, hp.y-1, hp.z, ho->id); delObjRect(hp.x-2, hp.y-1, hp.z, ho->id); } else if (details.end.x == details.start.x && details.end.y-1 == details.start.y) //b { delObjRect(hp.x, hp.y-1, hp.z, ho->id); delObjRect(hp.x-1, hp.y-1, hp.z, ho->id); delObjRect(hp.x-2, hp.y-1, hp.z, ho->id); } else if (details.end.x+1 == details.start.x && details.end.y-1 == details.start.y) //bl { delObjRect(hp.x, hp.y-1, hp.z, ho->id); delObjRect(hp.x-1, hp.y-1, hp.z, ho->id); delObjRect(hp.x-2, hp.y-1, hp.z, ho->id); delObjRect(hp.x-3, hp.y-1, hp.z, ho->id); delObjRect(hp.x, hp.y, hp.z, ho->id); delObjRect(hp.x, hp.y+1, hp.z, ho->id); } else if (details.end.x+1 == details.start.x && details.end.y == details.start.y) //l { delObjRect(hp.x, hp.y-1, hp.z, ho->id); delObjRect(hp.x, hp.y, hp.z, ho->id); } //restoring good rects subRect(details.end.x-2, details.end.y-1, details.end.z, genRect(32, 32, 0, 0), ho->id); subRect(details.end.x-1, details.end.y-1, details.end.z, genRect(32, 32, 32, 0), ho->id); subRect(details.end.x, details.end.y-1, details.end.z, genRect(32, 32, 64, 0), ho->id); subRect(details.end.x-2, details.end.y, details.end.z, genRect(32, 32, 0, 32), ho->id); subRect(details.end.x-1, details.end.y, details.end.z, genRect(32, 32, 32, 32), ho->id); subRect(details.end.x, details.end.y, details.end.z, genRect(32, 32, 64, 32), ho->id); //restoring good order of objects std::stable_sort(CGI->mh->ttiles[details.end.x-2][details.end.y-1][details.end.z].objects.begin(), CGI->mh->ttiles[details.end.x-2][details.end.y-1][details.end.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[details.end.x-1][details.end.y-1][details.end.z].objects.begin(), CGI->mh->ttiles[details.end.x-1][details.end.y-1][details.end.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[details.end.x][details.end.y-1][details.end.z].objects.begin(), CGI->mh->ttiles[details.end.x][details.end.y-1][details.end.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[details.end.x-2][details.end.y][details.end.z].objects.begin(), CGI->mh->ttiles[details.end.x-2][details.end.y][details.end.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[details.end.x-1][details.end.y][details.end.z].objects.begin(), CGI->mh->ttiles[details.end.x-1][details.end.y][details.end.z].objects.end(), objectBlitOrderSorter); std::stable_sort(CGI->mh->ttiles[details.end.x][details.end.y][details.end.z].objects.begin(), CGI->mh->ttiles[details.end.x][details.end.y][details.end.z].objects.end(), objectBlitOrderSorter); } void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult ) { EVENT_HANDLER_CALLED_BY_CLIENT; if (player == playerID) { if (victoryLossCheckResult.loss()) showInfoDialog(CGI->generaltexth->allTexts[95]); if (LOCPLINT == this) { GH.curInt = this; //waiting for dialogs requires this to get events waitForAllDialogs(); //wait till all dialogs are displayed and closed } if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) //all human players eliminated { if(adventureInt) { GH.terminate_cond->setn(true); adventureInt->deactivate(); if (GH.topInt() == adventureInt) GH.popInt(adventureInt); adventureInt.reset(); } } if (victoryLossCheckResult.victory() && LOCPLINT == this) { // end game if current human player has won requestReturningToMainMenu(true); } else if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) { //all human players eliminated requestReturningToMainMenu(false); } if (GH.curInt == this) GH.curInt = nullptr; } else { if (victoryLossCheckResult.loss() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME) //enemy has lost { std::string str = victoryLossCheckResult.messageToSelf; boost::algorithm::replace_first(str, "%s", CGI->generaltexth->capColors[player.getNum()]); showInfoDialog(str, std::vector>(1, std::make_shared(CComponent::flag, player.getNum(), 0))); } } } void CPlayerInterface::playerBonusChanged( const Bonus &bonus, bool gain ) { EVENT_HANDLER_CALLED_BY_CLIENT; } void CPlayerInterface::showPuzzleMap() { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); //TODO: interface should not know the real position of Grail... double ratio = 0; int3 grailPos = cb->getGrailPos(&ratio); GH.pushIntT(grailPos, ratio); } void CPlayerInterface::viewWorldMap() { adventureInt->changeMode(EAdvMapMode::WORLD_VIEW); } void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, int spellID) { EVENT_HANDLER_CALLED_BY_CLIENT; if(dynamic_cast(GH.topInt().get())) GH.popInts(1); if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK) eraseCurrentPathOf(caster, false); const CSpell * spell = CGI->spellh->objects.at(spellID); if(spellID == SpellID::VIEW_EARTH) { //TODO: implement on server side int level = caster->getSpellSchoolLevel(spell); adventureInt->worldViewOptions.showAllTerrain = (level>2); } auto castSoundPath = spell->getCastSound(); if(!castSoundPath.empty()) CCS->soundh->playSound(castSoundPath); } void CPlayerInterface::eraseCurrentPathOf(const CGHeroInstance * ho, bool checkForExistanceOfPath) { if (checkForExistanceOfPath) { assert(vstd::contains(paths, ho)); } else if (!vstd::contains(paths, ho)) { return; } assert(ho == adventureInt->selection); paths.erase(ho); adventureInt->terrain.currentPath = nullptr; adventureInt->updateMoveHero(ho, false); } void CPlayerInterface::removeLastNodeFromPath(const CGHeroInstance *ho) { adventureInt->terrain.currentPath->nodes.erase(adventureInt->terrain.currentPath->nodes.end()-1); if (adventureInt->terrain.currentPath->nodes.size() < 2) //if it was the last one, remove entire path and path with only one tile is not a real path eraseCurrentPathOf(ho); } CGPath * CPlayerInterface::getAndVerifyPath(const CGHeroInstance * h) { if (vstd::contains(paths,h)) //hero has assigned path { CGPath &path = paths[h]; if (!path.nodes.size()) { logGlobal->warn("Warning: empty path found..."); paths.erase(h); } else { assert(h->getPosition(false) == path.startPos()); //update the hero path in case of something has changed on map if (LOCPLINT->cb->getPathsInfo(h)->getPath(path, path.endPos())) return &path; else paths.erase(h); } } return nullptr; } void CPlayerInterface::acceptTurn() { bool centerView = true; if (settings["session"]["autoSkip"].Bool()) { centerView = false; while(CInfoWindow *iw = dynamic_cast(GH.topInt().get())) iw->close(); } waitWhileDialog(); if(CSH->howManyPlayerInterfaces() > 1) adventureInt->startTurn(); adventureInt->heroList.update(); adventureInt->townList.update(); const CGHeroInstance * heroToSelect = nullptr; // find first non-sleeping hero for (auto hero : wanderingHeroes) { if (boost::range::find(sleepingHeroes, hero) == sleepingHeroes.end()) { heroToSelect = hero; break; } } //select first hero if available. if (heroToSelect != nullptr) { adventureInt->select(heroToSelect, centerView); } else if (towns.size()) adventureInt->select(towns.front(), centerView); else adventureInt->select(wanderingHeroes.front()); //show new day animation and sound on infobar adventureInt->infoBar.showDate(); adventureInt->updateNextHero(nullptr); adventureInt->showAll(screen); if(settings["session"]["autoSkip"].Bool() && !LOCPLINT->shiftPressed()) { if(CInfoWindow *iw = dynamic_cast(GH.topInt().get())) iw->close(); adventureInt->fendTurn(); } // warn player if he has no town if (cb->howManyTowns() == 0) { auto playerColor = *cb->getPlayerID(); std::vector components; components.push_back(Component(Component::FLAG, playerColor.getNum(), 0, 0)); MetaString text; const auto & optDaysWithoutCastle = cb->getPlayer(playerColor)->daysWithoutCastle; if(optDaysWithoutCastle) { auto daysWithoutCastle = optDaysWithoutCastle.get(); if (daysWithoutCastle < 6) { text.addTxt(MetaString::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land. text.addReplacement(MetaString::COLOR, playerColor.getNum()); text.addReplacement(7 - daysWithoutCastle); } else if (daysWithoutCastle == 6) { text.addTxt(MetaString::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land. text.addReplacement(MetaString::COLOR, playerColor.getNum()); } showInfoDialogAndWait(components, text); } else logGlobal->warn("Player has no towns, but daysWithoutCastle is not set"); } } void CPlayerInterface::tryDiggging(const CGHeroInstance *h) { std::string hlp; CGI->mh->getTerrainDescr(h->getPosition(false), hlp, false); auto isDiggingPossible = h->diggingStatus(); if (hlp.length()) isDiggingPossible = EDiggingStatus::TILE_OCCUPIED; //TODO integrate with canDig int msgToShow = -1; switch(isDiggingPossible) { case EDiggingStatus::CAN_DIG: break; case EDiggingStatus::LACK_OF_MOVEMENT: msgToShow = 56; //"Digging for artifacts requires a whole day, try again tomorrow." break; case EDiggingStatus::TILE_OCCUPIED: msgToShow = 97; //Try searching on clear ground. break; case EDiggingStatus::WRONG_TERRAIN: msgToShow = 60; ////Try looking on land! break; default: assert(0); } if (msgToShow < 0) cb->dig(h); else showInfoDialog(CGI->generaltexth->allTexts[msgToShow]); } void CPlayerInterface::updateInfo(const CGObjectInstance * specific) { adventureInt->infoBar.showSelection(); } void CPlayerInterface::battleNewRoundFirst( int round ) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->newRoundFirst(round); } void CPlayerInterface::stopMovement() { if (stillMoveHero.get() == DURING_MOVE)//if we are in the middle of hero movement stillMoveHero.setn(STOP_MOVE); //after showing dialog movement will be stopped } void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) { EVENT_HANDLER_CALLED_BY_CLIENT; if (market->o->ID == Obj::ALTAR_OF_SACRIFICE) { //EEMarketMode mode = market->availableModes().front(); if (market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL) GH.pushIntT(market, visitor, EMarketMode::ARTIFACT_EXP); else if (market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD) GH.pushIntT(market, visitor, EMarketMode::CREATURE_EXP); } else { GH.pushIntT(market, visitor, market->availableModes().front()); } } void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) { EVENT_HANDLER_CALLED_BY_CLIENT; GH.pushIntT(visitor, market); } void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) { EVENT_HANDLER_CALLED_BY_CLIENT; GH.pushIntT(visitor, object); } void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm) { EVENT_HANDLER_CALLED_BY_CLIENT; if (CMarketplaceWindow *cmw = dynamic_cast(GH.topInt().get())) cmw->artifactsChanged(false); } void CPlayerInterface::showTavernWindow(const CGObjectInstance *townOrTavern) { EVENT_HANDLER_CALLED_BY_CLIENT; GH.pushIntT(townOrTavern); } void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj) { EVENT_HANDLER_CALLED_BY_CLIENT; GH.pushIntT(obj); } void CPlayerInterface::showQuestLog() { EVENT_HANDLER_CALLED_BY_CLIENT; GH.pushIntT(LOCPLINT->cb->getMyQuests()); } void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj) { if (obj->shipyardStatus() != IBoatGenerator::GOOD) { MetaString txt; obj->getProblemText(txt); showInfoDialog(txt.toString()); } else showShipyardDialog(obj); } void CPlayerInterface::requestReturningToMainMenu(bool won) { CSH->state = EClientState::DISCONNECTING; CCS->soundh->ambientStopAllChannels(); if(won && cb->getStartInfo()->campState) CSH->startCampaignScenario(cb->getStartInfo()->campState); else sendCustomEvent(EUserEvent::RETURN_TO_MAIN_MENU); } void CPlayerInterface::sendCustomEvent( int code ) { CGuiHandler::pushSDLEvent(SDL_USEREVENT, code); } void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al) { auto hero = boost::apply_visitor(HeroObjectRetriever(), al.artHolder); if(hero) { auto art = hero->getArt(al.slot); if(art == nullptr) { logGlobal->error("artifact location %d points to nothing", al.slot.num); return; } CHeroArtPlace::askToAssemble(art, al.slot, hero); } } void CPlayerInterface::artifactPut(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); askToAssembleArtifact(al); } void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); for(auto isa : GH.listInt) { auto artWin = dynamic_cast(isa.get()); if (artWin) artWin->artifactRemoved(al); } } void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); for(auto isa : GH.listInt) { auto artWin = dynamic_cast(isa.get()); if (artWin) artWin->artifactMoved(src, dst); } askToAssembleArtifact(dst); } void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); for(auto isa : GH.listInt) { auto artWin = dynamic_cast(isa.get()); if (artWin) artWin->artifactAssembled(al); } } void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); for(auto isa : GH.listInt) { auto artWin = dynamic_cast(isa.get()); if (artWin) artWin->artifactDisassembled(al); } } void CPlayerInterface::playerStartsTurn(PlayerColor player) { EVENT_HANDLER_CALLED_BY_CLIENT; if (!vstd::contains (GH.listInt, adventureInt)) { GH.popInts (GH.listInt.size()); //after map load - remove everything else GH.pushInt (adventureInt); } else { adventureInt->infoBar.showSelection(); while (GH.listInt.front() != adventureInt && !dynamic_cast(GH.listInt.front().get())) //don't remove dialogs that expect query answer GH.popInts(1); } if(CSH->howManyPlayerInterfaces() == 1) { GH.curInt = this; adventureInt->startTurn(); } if (player != playerID && this == LOCPLINT) { waitWhileDialog(); adventureInt->aiTurnStarted(); } } void CPlayerInterface::waitForAllDialogs(bool unlockPim) { while(!dialogs.empty()) { auto unlock = vstd::makeUnlockGuardIf(*pim, unlockPim); SDL_Delay(5); } waitWhileDialog(unlockPim); } void CPlayerInterface::proposeLoadingGame() { showYesNoDialog(CGI->generaltexth->allTexts[68], [this](){ sendCustomEvent(EUserEvent::RETURN_TO_MENU_LOAD); }, nullptr); } CPlayerInterface::SpellbookLastSetting::SpellbookLastSetting() { spellbookLastPageBattle = spellbokLastPageAdvmap = 0; spellbookLastTabBattle = spellbookLastTabAdvmap = 4; } bool CPlayerInterface::capturedAllEvents() { if (duringMovement) { //just inform that we are capturing events. they will be processed by heroMoved() in client thread. return true; } if (ignoreEvents) { boost::unique_lock un(eventsM); while(!events.empty()) { events.pop(); } return true; } return false; } void CPlayerInterface::setMovementStatus(bool value) { duringMovement = value; if (value) { CCS->curh->hide(); } else { CCS->curh->show(); } } void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) { int i = 1; auto getObj = [&](int3 coord, bool ignoreHero) { return cb->getTile(CGHeroInstance::convertPosition(coord,false))->topVisitableObj(ignoreHero); }; auto isTeleportAction = [&](CGPathNode::ENodeAction action) -> bool { if (action != CGPathNode::TELEPORT_NORMAL && action != CGPathNode::TELEPORT_BLOCKING_VISIT && action != CGPathNode::TELEPORT_BATTLE) { return false; } return true; }; auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * { if (CGTeleport::isConnected(currentObject, nextObjectTop)) return nextObjectTop; if (nextObjectTop && nextObjectTop->ID == Obj::HERO && CGTeleport::isConnected(currentObject, nextObject)) { return nextObject; } return nullptr; }; boost::unique_lock un(stillMoveHero.mx); stillMoveHero.data = CONTINUE_MOVE; auto doMovement = [&](int3 dst, bool transit) { stillMoveHero.data = WAITING_MOVE; cb->moveHero(h, dst, transit); while(stillMoveHero.data != STOP_MOVE && stillMoveHero.data != CONTINUE_MOVE) stillMoveHero.cond.wait(un); }; { path.convert(0); ETerrainType currentTerrain = ETerrainType::BORDER; // not init yet ETerrainType newTerrain; int sh = -1; auto canStop = [&](CGPathNode * node) -> bool { if (node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL) return true; if (node->accessible == CGPathNode::ACCESSIBLE) return true; return false; }; for (i=path.nodes.size()-1; i>0 && (stillMoveHero.data == CONTINUE_MOVE || !canStop(&path.nodes[i])); i--) { int3 currentCoord = path.nodes[i].coord; int3 nextCoord = path.nodes[i-1].coord; auto currentObject = getObj(currentCoord, currentCoord == h->pos); auto nextObjectTop = getObj(nextCoord, false); auto nextObject = getObj(nextCoord, true); auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject); if (isTeleportAction(path.nodes[i-1].action) && destTeleportObj != nullptr) { CCS->soundh->stopSound(sh); destinationTeleport = destTeleportObj->id; destinationTeleportPos = nextCoord; doMovement(h->pos, false); if (path.nodes[i-1].action == CGPathNode::TELEPORT_BLOCKING_VISIT) { destinationTeleport = ObjectInstanceID(); destinationTeleportPos = int3(-1); } if(i != path.nodes.size() - 1) sh = CCS->soundh->playSound(CCS->soundh->horseSounds[currentTerrain], -1); continue; } if (path.nodes[i-1].turns) { //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) stillMoveHero.data = STOP_MOVE; break; } // Start a new sound for the hero movement or let the existing one carry on. #if 0 // TODO if (hero is flying && sh == -1) sh = CCS->soundh->playSound(soundBase::horseFlying, -1); #endif { newTerrain = cb->getTile(CGHeroInstance::convertPosition(currentCoord, false))->terType; if (newTerrain != currentTerrain) { CCS->soundh->stopSound(sh); sh = CCS->soundh->playSound(CCS->soundh->horseSounds[newTerrain], -1); currentTerrain = newTerrain; } } assert(h->pos.z == nextCoord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all int3 endpos(nextCoord.x, nextCoord.y, h->pos.z); logGlobal->trace("Requesting hero movement to %s", endpos.toString()); bool useTransit = false; if ((i-2 >= 0) // Check there is node after next one; otherwise transit is pointless && (CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i-2].coord, false)) || CGTeleport::isTeleport(nextObjectTop))) { // Hero should be able to go through object if it's allow transit useTransit = true; } else if (path.nodes[i-1].layer == EPathfindingLayer::AIR) useTransit = true; doMovement(endpos, useTransit); logGlobal->trace("Resuming %s", __FUNCTION__); bool guarded = cb->isInTheMap(cb->getGuardingCreaturePosition(endpos - int3(1, 0, 0))); if ((!useTransit && guarded) || showingDialog->get() == true) // Abort movement if a guard was fought or there is a dialog to display (Mantis #1136) break; } CCS->soundh->stopSound(sh); } //Update cursor so icon can change if needed when it reappears; doesn;'t apply if a dialog box pops up at the end of the movement if (!showingDialog->get()) GH.fakeMouseMove(); //todo: this should be in main thread if (adventureInt) { // (i == 0) means hero went through all the path adventureInt->updateMoveHero(h, (i != 0)); adventureInt->updateNextHero(h); } setMovementStatus(false); } void CPlayerInterface::showWorldViewEx(const std::vector& objectPositions) { EVENT_HANDLER_CALLED_BY_CLIENT; //TODO: showWorldViewEx std::copy(objectPositions.begin(), objectPositions.end(), std::back_inserter(adventureInt->worldViewOptions.iconPositions)); viewWorldMap(); } void CPlayerInterface::updateAmbientSounds(bool resetAll) { if(castleInt || battleInt || !makingTurn || !currentSelection) { CCS->soundh->ambientStopAllChannels(); return; } else if(!dynamic_cast(GH.topInt().get())) { return; } if(resetAll) CCS->soundh->ambientStopAllChannels(); std::map currentSounds; auto updateSounds = [&](std::string soundId, int distance) -> void { if(vstd::contains(currentSounds, soundId)) currentSounds[soundId] = std::max(currentSounds[soundId], distance); else currentSounds.insert(std::make_pair(soundId, distance)); }; int3 pos = currentSelection->getSightCenter(); std::unordered_set tiles; cb->getVisibleTilesInRange(tiles, pos, CCS->soundh->ambientGetRange(), int3::DIST_CHEBYSHEV); for(int3 tile : tiles) { int dist = pos.dist(tile, int3::DIST_CHEBYSHEV); // We want sound for every special terrain on tile and not just one on top for(auto & ttObj : CGI->mh->ttiles[tile.x][tile.y][tile.z].objects) { if(ttObj.ambientSound) updateSounds(ttObj.ambientSound.get(), dist); } if(CGI->mh->map->isCoastalTile(tile)) updateSounds("LOOPOCEA", dist); } CCS->soundh->ambientUpdateChannels(currentSounds); } vcmi-0.99+git20190113.f06c8a87/client/CPlayerInterface.h000066400000000000000000000342501342332007200217650ustar00rootroot00000000000000/* * CPlayerInterface.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../lib/FunctionList.h" #include "../lib/CGameInterface.h" #include "../lib/NetPacksBase.h" #include "gui/CIntObject.h" #ifdef __GNUC__ #define sprintf_s snprintf #endif class CButton; class CToggleGroup; struct TryMoveHero; class CGHeroInstance; class CAdvMapInt; class CCastleInterface; class CBattleInterface; class CStack; class CComponent; class CCreature; struct SDL_Surface; struct CGPath; class CCreatureAnimation; class CSelectableComponent; class CCreatureSet; class CGObjectInstance; class CSlider; struct UpgradeInfo; template struct CondSh; class CInGameConsole; class CInGameConsole; union SDL_Event; class CInfoWindow; class IShowActivatable; class ClickableL; class ClickableR; class Hoverable; class KeyInterested; class MotionInterested; class TimeInterested; class IShowable; struct CPathsInfo; namespace boost { class mutex; class recursive_mutex; } /// Central class for managing user interface logic class CPlayerInterface : public CGameInterface, public IUpdateable { const CArmedInstance * currentSelection; public: ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation int3 destinationTeleportPos; //minor interfaces CondSh *showingDialog; //indicates if dialog box is displayed static boost::recursive_mutex *pim; bool makingTurn; //if player is already making his turn int firstCall; // -1 - just loaded game; 1 - just started game; 0 otherwise int autosaveCount; static const int SAVES_COUNT = 5; CCastleInterface * castleInt; //nullptr if castle window isn't opened static CBattleInterface * battleInt; //nullptr if no battle CInGameConsole * cingconsole; std::shared_ptr cb; //to communicate with engine const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr) std::list> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) std::vector wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones) std::vector towns; //our towns on the adventure map std::map paths; //maps hero => selected path in adventure map std::vector sleepingHeroes; //if hero is in here, he's sleeping //During battle is quick combat mode is used std::shared_ptr autofightingAI; //AI that makes decisions bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface. const CArmedInstance * getSelection(); void setSelection(const CArmedInstance * obj); struct SpellbookLastSetting { int spellbookLastPageBattle, spellbokLastPageAdvmap; //on which page we left spellbook int spellbookLastTabBattle, spellbookLastTabAdvmap; //on which page we left spellbook SpellbookLastSetting(); template void serialize( Handler &h, const int version ) { h & spellbookLastPageBattle; h & spellbokLastPageAdvmap; h & spellbookLastTabBattle; h & spellbookLastTabAdvmap; } } spellbookSettings; void update() override; void initializeHeroTownList(); int getLastIndex(std::string namePrefix); //overridden funcs from CGameInterface void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; //what: 1 - built, 2 - demolished void artifactPut(const ArtifactLocation &al) override; void artifactRemoved(const ArtifactLocation &al) override; void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) override; void artifactAssembled(const ArtifactLocation &al) override; void artifactDisassembled(const ArtifactLocation &al) override; void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; void heroCreated(const CGHeroInstance* hero) override; void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) override; void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; void heroInGarrisonChange(const CGTownInstance *town) override; void heroMoved(const TryMoveHero & details) override; void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; void heroManaPointsChanged(const CGHeroInstance * hero) override; void heroMovePointsChanged(const CGHeroInstance * hero) override; void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override; void receivedResource() override; void showInfoDialog(const std::string & text, const std::vector & components, int soundID) override; void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) override; void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard; void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; void showPuzzleMap() override; void viewWorldMap() override; void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) override; void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) override; void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override; void showTavernWindow(const CGObjectInstance *townOrTavern) override; void showThievesGuildWindow (const CGObjectInstance * obj) override; void showQuestLog() override; void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; //called when a hero casts a spell void tileHidden(const std::unordered_set &pos) override; //called when given tiles become hidden under fog of war void tileRevealed(const std::unordered_set &pos) override; //called when fog of war disappears from given tiles void newObject(const CGObjectInstance * obj) override; void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) void yourTurn() override; void availableCreaturesChanged(const CGDwelling *town) override; void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain) override;//if gain hero received bonus, else he lost it void playerBonusChanged(const Bonus &bonus, bool gain) override; void requestRealized(PackageApplied *pa) override; void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; void centerView (int3 pos, int focusTime) override; void objectPropertyChanged(const SetObjectProperty * sop) override; void objectRemoved(const CGObjectInstance *obj) override; void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface void showComp(const Component &comp, std::string message) override; //display component in the advmapint infobox void saveGame(BinarySerializer & h, const int version) override; //saving void loadGame(BinaryDeserializer & h, const int version) override; //loading void showWorldViewEx(const std::vector & objectPositions) override; //for battles void actionFinished(const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero void actionStarted(const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack void battleAttack(const BattleAttack *ba) override; //stack performs attack void battleEnd(const BattleResult *br) override; //end of battle void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; used for HP regen handling void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn void battleStackMoved(const CStack * stack, std::vector dest, int distance) override; void battleSpellCast(const BattleSpellCast *sc) override; void battleStacksEffectsSet(const SetStackEffect & sse) override; //called when a specific effect is set to stacks void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect void battleStacksAttacked(const std::vector & bsa, const std::vector & battleLog) override; void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleUnitsChanged(const std::vector & units, const std::vector & customEffects, const std::vector & battleLog) override; void battleObstaclesChanged(const std::vector & obstacles) override; void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleGateStateChanged(const EGateState state) override; void yourTacticPhase(int distance) override; //-------------// void showArtifactAssemblyDialog(ui32 artifactID, ui32 assembleTo, bool assemble, CFunctionList onYes, CFunctionList onNo); void garrisonsChanged(std::vector objs); void garrisonChanged(const CGObjectInstance * obj); void heroKilled(const CGHeroInstance* hero); void waitWhileDialog(bool unlockPim = true); void waitForAllDialogs(bool unlockPim = true); bool shiftPressed() const; //determines if shift key is pressed (left or right or both) bool ctrlPressed() const; //determines if ctrl key is pressed (left or right or both) bool altPressed() const; //determines if alt key is pressed (left or right or both) void redrawHeroWin(const CGHeroInstance * hero); void openTownWindow(const CGTownInstance * town); //shows townscreen void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero void updateInfo(const CGObjectInstance * specific); void init(std::shared_ptr CB) override; int3 repairScreenPos(int3 pos); //returns position closest to pos we can center screen on void activateForSpectator(); // TODO: spectator probably need own player interface class // show dialogs void showInfoDialog(const std::string &text, std::shared_ptr component); void showInfoDialog(const std::string &text, const std::vector> & components = std::vector>(), int soundID = 0); void showInfoDialogAndWait(std::vector & components, const MetaString & text); void showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components = std::vector>()); void stopMovement(); void moveHero(const CGHeroInstance *h, CGPath path); void initMovement(const TryMoveHero &details, const CGHeroInstance * ho, const int3 &hp );//initializing objects and performing first step of move void movementPxStep( const TryMoveHero &details, int i, const int3 &hp, const CGHeroInstance * ho );//performing step of movement void finishMovement( const TryMoveHero &details, const int3 &hp, const CGHeroInstance * ho ); //finish movement void eraseCurrentPathOf( const CGHeroInstance * ho, bool checkForExistanceOfPath = true ); void removeLastNodeFromPath(const CGHeroInstance *ho); CGPath *getAndVerifyPath( const CGHeroInstance * h ); void acceptTurn(); //used during hot seat after your turn message is close void tryDiggging(const CGHeroInstance *h); void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard; void requestReturningToMainMenu(bool won); void sendCustomEvent(int code); void proposeLoadingGame(); // Ambient sounds void updateAmbientSounds(bool resetAll = false); ///returns true if all events are processed internally bool capturedAllEvents(); CPlayerInterface(PlayerColor Player); ~CPlayerInterface(); private: template void serializeTempl(Handler &h, const int version); private: struct IgnoreEvents { CPlayerInterface & owner; IgnoreEvents(CPlayerInterface & Owner):owner(Owner) { owner.ignoreEvents = true; }; ~IgnoreEvents() { owner.ignoreEvents = false; }; }; bool duringMovement; bool ignoreEvents; void doMoveHero(const CGHeroInstance *h, CGPath path); void setMovementStatus(bool value); void askToAssembleArtifact(const ArtifactLocation &al); }; extern CPlayerInterface * LOCPLINT; vcmi-0.99+git20190113.f06c8a87/client/CServerHandler.cpp000066400000000000000000000420141342332007200220040ustar00rootroot00000000000000/* * CServerHandler.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CServerHandler.h" #include "Client.h" #include "CGameInfo.h" #include "CPlayerInterface.h" #include "gui/CGuiHandler.h" #include "lobby/CSelectionBase.h" #include "lobby/CLobbyScreen.h" #include "mainmenu/CMainMenu.h" #ifndef VCMI_ANDROID #include "../lib/Interprocess.h" #else #include "../lib/CAndroidVMHelper.h" #endif #include "../lib/CConfigHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CThreadHelper.h" #include "../lib/NetPacks.h" #include "../lib/StartInfo.h" #include "../lib/VCMIDirs.h" #include "../lib/mapping/CCampaignHandler.h" #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/mapObjects/MiscObjects.h" #include "../lib/rmg/CMapGenOptions.h" #include "../lib/registerTypes/RegisterTypes.h" #include "../lib/serializer/Connection.h" #include "../lib/serializer/CMemorySerializer.h" #include #include #include #include "../lib/serializer/Cast.h" template class CApplyOnLobby; #ifdef VCMI_ANDROID extern std::atomic_bool androidTestServerReadyFlag; #endif class CBaseForLobbyApply { public: virtual bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const = 0; virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const = 0; virtual ~CBaseForLobbyApply(){}; template static CBaseForLobbyApply * getApplier(const U * t = nullptr) { return new CApplyOnLobby(); } }; template class CApplyOnLobby : public CBaseForLobbyApply { public: bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override { T * ptr = static_cast(pack); logNetwork->trace("\tImmidiately apply on lobby: %s", typeList.getTypeInfo(ptr)->name()); return ptr->applyOnLobbyHandler(handler); } void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override { T * ptr = static_cast(pack); logNetwork->trace("\tApply on lobby from queue: %s", typeList.getTypeInfo(ptr)->name()); ptr->applyOnLobbyScreen(lobby, handler); } }; template<> class CApplyOnLobby: public CBaseForLobbyApply { public: bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override { logGlobal->error("Cannot apply plain CPack!"); assert(0); return false; } void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override { logGlobal->error("Cannot apply plain CPack!"); assert(0); } }; extern std::string NAME; CServerHandler::CServerHandler() : state(EClientState::NONE), mx(std::make_shared()), client(nullptr), loadMode(0), campaignStateToSend(nullptr) { uuid = boost::uuids::to_string(boost::uuids::random_generator()()); applier = std::make_shared>(); registerTypesLobbyPacks(*applier); } void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::vector * names) { hostClientId = -1; state = EClientState::NONE; th = make_unique(); packsForLobbyScreen.clear(); c.reset(); si = std::make_shared(); playerNames.clear(); si->difficulty = 1; si->mode = mode; myNames.clear(); if(names && !names->empty()) //if have custom set of player names - use it myNames = *names; else myNames.push_back(settings["general"]["playerName"].String()); #ifndef VCMI_ANDROID shm.reset(); if(!settings["session"]["disable-shm"].Bool()) { std::string sharedMemoryName = "vcmi_memory"; if(settings["session"]["enable-shm-uuid"].Bool()) { //used or automated testing when multiple clients start simultaneously sharedMemoryName += "_" + uuid; } try { shm = std::make_shared(sharedMemoryName, true); } catch(...) { shm.reset(); logNetwork->error("Cannot open interprocess memory. Continue without it..."); } } #endif } void CServerHandler::startLocalServerAndConnect() { if(threadRunLocalServer) threadRunLocalServer->join(); th->update(); #ifdef VCMI_ANDROID { CAndroidVMHelper envHelper; envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "startServer", true); } #else threadRunLocalServer = std::make_shared(&CServerHandler::threadRunServer, this); //runs server executable; #endif logNetwork->trace("Setting up thread calling server: %d ms", th->getDiff()); th->update(); #ifndef VCMI_ANDROID if(shm) shm->sr->waitTillReady(); #else logNetwork->info("waiting for server"); while(!androidTestServerReadyFlag.load()) { logNetwork->info("still waiting..."); boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); } logNetwork->info("waiting for server finished..."); androidTestServerReadyFlag = false; #endif logNetwork->trace("Waiting for server: %d ms", th->getDiff()); th->update(); //put breakpoint here to attach to server before it does something stupid #ifndef VCMI_ANDROID justConnectToServer(settings["server"]["server"].String(), shm ? shm->sr->port : 0); #else justConnectToServer(settings["server"]["server"].String()); #endif logNetwork->trace("\tConnecting to the server: %d ms", th->getDiff()); } void CServerHandler::justConnectToServer(const std::string & addr, const ui16 port) { state = EClientState::CONNECTING; while(!c && state != EClientState::CONNECTION_CANCELLED) { try { logNetwork->info("Establishing connection..."); c = std::make_shared( addr.size() ? addr : settings["server"]["server"].String(), port ? port : getDefaultPort(), NAME, uuid); } catch(...) { logNetwork->error("\nCannot establish connection! Retrying within 1 second"); boost::this_thread::sleep(boost::posix_time::seconds(1)); } } if(state == EClientState::CONNECTION_CANCELLED) logNetwork->info("Connection aborted by player!"); else c->handler = std::make_shared(&CServerHandler::threadHandleConnection, this); } void CServerHandler::applyPacksOnLobbyScreen() { if(!c || !c->handler) return; boost::unique_lock lock(*mx); while(!packsForLobbyScreen.empty()) { CPackForLobby * pack = packsForLobbyScreen.front(); packsForLobbyScreen.pop_front(); CBaseForLobbyApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier apply->applyOnLobbyScreen(static_cast(SEL), this, pack); GH.totalRedraw(); delete pack; } } void CServerHandler::stopServerConnection() { if(c->handler) { while(!c->handler->timed_join(boost::posix_time::milliseconds(50))) applyPacksOnLobbyScreen(); c->handler->join(); } } std::set CServerHandler::getHumanColors() { return clientHumanColors(c->connectionID); } PlayerColor CServerHandler::myFirstColor() const { return clientFirstColor(c->connectionID); } bool CServerHandler::isMyColor(PlayerColor color) const { return isClientColor(c->connectionID, color); } ui8 CServerHandler::myFirstId() const { return clientFirstId(c->connectionID); } bool CServerHandler::isServerLocal() const { if(threadRunLocalServer) return true; return false; } bool CServerHandler::isHost() const { return c && hostClientId == c->connectionID; } bool CServerHandler::isGuest() const { return !c || hostClientId != c->connectionID; } ui16 CServerHandler::getDefaultPort() { if(settings["session"]["serverport"].Integer()) return settings["session"]["serverport"].Integer(); else return settings["server"]["port"].Integer(); } std::string CServerHandler::getDefaultPortStr() { return boost::lexical_cast(getDefaultPort()); } void CServerHandler::sendClientConnecting() const { LobbyClientConnected lcc; lcc.uuid = uuid; lcc.names = myNames; lcc.mode = si->mode; sendLobbyPack(lcc); } void CServerHandler::sendClientDisconnecting() { // FIXME: This is workaround needed to make sure client not trying to sent anything to non existed server if(state == EClientState::DISCONNECTING) return; state = EClientState::DISCONNECTING; LobbyClientDisconnected lcd; lcd.clientId = c->connectionID; logNetwork->info("Connection has been requested to be closed."); if(isServerLocal()) { lcd.shutdownServer = true; logNetwork->info("Sent closing signal to the server"); } else { logNetwork->info("Sent leaving signal to the server"); } sendLobbyPack(lcd); } void CServerHandler::setCampaignState(std::shared_ptr newCampaign) { state = EClientState::LOBBY_CAMPAIGN; LobbySetCampaign lsc; lsc.ourCampaign = newCampaign; sendLobbyPack(lsc); } void CServerHandler::setCampaignMap(int mapId) const { if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place return; LobbySetCampaignMap lscm; lscm.mapId = mapId; sendLobbyPack(lscm); } void CServerHandler::setCampaignBonus(int bonusId) const { if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place return; LobbySetCampaignBonus lscb; lscb.bonusId = bonusId; sendLobbyPack(lscb); } void CServerHandler::setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts) const { LobbySetMap lsm; lsm.mapInfo = to; lsm.mapGenOpts = mapGenOpts; sendLobbyPack(lsm); } void CServerHandler::setPlayer(PlayerColor color) const { LobbySetPlayer lsp; lsp.clickedColor = color; sendLobbyPack(lsp); } void CServerHandler::setPlayerOption(ui8 what, ui8 dir, PlayerColor player) const { LobbyChangePlayerOption lcpo; lcpo.what = what; lcpo.direction = dir; lcpo.color = player; sendLobbyPack(lcpo); } void CServerHandler::setDifficulty(int to) const { LobbySetDifficulty lsd; lsd.difficulty = to; sendLobbyPack(lsd); } void CServerHandler::setTurnLength(int npos) const { vstd::amin(npos, GameConstants::POSSIBLE_TURNTIME.size() - 1); LobbySetTurnTime lstt; lstt.turnTime = GameConstants::POSSIBLE_TURNTIME[npos]; sendLobbyPack(lstt); } void CServerHandler::sendMessage(const std::string & txt) const { std::istringstream readed; readed.str(txt); std::string command; readed >> command; if(command == "!passhost") { std::string id; readed >> id; if(id.length()) { LobbyChangeHost lch; lch.newHostConnectionId = boost::lexical_cast(id); sendLobbyPack(lch); } } else if(command == "!forcep") { std::string connectedId, playerColorId; readed >> connectedId; readed >> playerColorId; if(connectedId.length(), playerColorId.length()) { ui8 connected = boost::lexical_cast(connectedId); auto color = PlayerColor(boost::lexical_cast(playerColorId)); if(color.isValidPlayer() && playerNames.find(connected) != playerNames.end()) { LobbyForceSetPlayer lfsp; lfsp.targetConnectedPlayer = connected; lfsp.targetPlayerColor = color; sendLobbyPack(lfsp); } } } else { LobbyChatMessage lcm; lcm.message = txt; lcm.playerName = playerNames.find(myFirstId())->second.name; sendLobbyPack(lcm); } } void CServerHandler::sendGuiAction(ui8 action) const { LobbyGuiAction lga; lga.action = static_cast(action); sendLobbyPack(lga); } void CServerHandler::sendStartGame(bool allowOnlyAI) const { verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); LobbyStartGame lsg; sendLobbyPack(lsg); } void CServerHandler::startGameplay() { if(CMM) CMM->disable(); client = new CClient(); switch(si->mode) { case StartInfo::NEW_GAME: client->newGame(); break; case StartInfo::CAMPAIGN: client->newGame(); break; case StartInfo::LOAD_GAME: client->loadGame(); break; default: throw std::runtime_error("Invalid mode"); } // After everything initialized we can accept CPackToClient netpacks c->enterGameplayConnectionMode(client->gameState()); state = EClientState::GAMEPLAY; } void CServerHandler::endGameplay(bool closeConnection, bool restart) { client->endGame(); vstd::clear_pointer(client); if(closeConnection) { // Game is ending // Tell the network thread to reach a stable state CSH->sendClientDisconnecting(); logNetwork->info("Closed connection."); } if(!restart) { if(CMM) { GH.curInt = CMM.get(); CMM->enable(); } else { GH.curInt = CMainMenu::create().get(); } } } void CServerHandler::startCampaignScenario(std::shared_ptr cs) { SDL_Event event; event.type = SDL_USEREVENT; event.user.code = EUserEvent::CAMPAIGN_START_SCENARIO; if(cs) event.user.data1 = CMemorySerializer::deepCopy(*cs.get()).release(); else event.user.data1 = CMemorySerializer::deepCopy(*si->campState.get()).release(); SDL_PushEvent(&event); } int CServerHandler::howManyPlayerInterfaces() { int playerInts = 0; for(auto pint : client->playerint) { if(dynamic_cast(pint.second.get())) playerInts++; } return playerInts; } ui8 CServerHandler::getLoadMode() { if(state == EClientState::GAMEPLAY) { if(si->campState) return ELoadMode::CAMPAIGN; for(auto pn : playerNames) { if(pn.second.connection != c->connectionID) return ELoadMode::MULTI; } return ELoadMode::SINGLE; } return loadMode; } void CServerHandler::debugStartTest(std::string filename, bool save) { logGlobal->info("Starting debug test with file: %s", filename); auto mapInfo = std::make_shared(); if(save) { resetStateForLobby(StartInfo::LOAD_GAME); mapInfo->saveInit(ResourceID(filename, EResType::CLIENT_SAVEGAME)); screenType = ESelectionScreen::loadGame; } else { resetStateForLobby(StartInfo::NEW_GAME); mapInfo->mapInit(filename); screenType = ESelectionScreen::newGame; } if(settings["session"]["donotstartserver"].Bool()) justConnectToServer("127.0.0.1", 3030); else startLocalServerAndConnect(); while(!settings["session"]["headless"].Bool() && !dynamic_cast(GH.topInt().get())) boost::this_thread::sleep(boost::posix_time::milliseconds(50)); while(!mi || mapInfo->fileURI != CSH->mi->fileURI) { setMapInfo(mapInfo); boost::this_thread::sleep(boost::posix_time::milliseconds(50)); } // "Click" on color to remove us from it setPlayer(myFirstColor()); while(myFirstColor() != PlayerColor::CANNOT_DETERMINE) boost::this_thread::sleep(boost::posix_time::milliseconds(50)); while(true) { try { sendStartGame(); break; } catch(...) { } boost::this_thread::sleep(boost::posix_time::milliseconds(50)); } } void CServerHandler::threadHandleConnection() { setThreadName("CServerHandler::threadHandleConnection"); c->enterLobbyConnectionMode(); try { sendClientConnecting(); while(c->connected) { while(state == EClientState::STARTING) boost::this_thread::sleep(boost::posix_time::milliseconds(10)); CPack * pack = c->retrievePack(); if(state == EClientState::DISCONNECTING) { // FIXME: server shouldn't really send netpacks after it's tells client to disconnect // Though currently they'll be delivered and might cause crash. vstd::clear_pointer(pack); } else if(auto lobbyPack = dynamic_ptr_cast(pack)) { if(applier->getApplier(typeList.getTypeID(pack))->applyOnLobbyHandler(this, pack)) { if(!settings["session"]["headless"].Bool()) { boost::unique_lock lock(*mx); packsForLobbyScreen.push_back(lobbyPack); } } } else if(auto clientPack = dynamic_ptr_cast(pack)) { client->handlePack(clientPack); } } } //catch only asio exceptions catch(const boost::system::system_error & e) { if(state == EClientState::DISCONNECTING) { logNetwork->info("Successfully closed connection to server, ending listening thread!"); } else { logNetwork->error("Lost connection to server, ending listening thread!"); logNetwork->error(e.what()); if(client) { CGuiHandler::pushSDLEvent(SDL_USEREVENT, EUserEvent::RETURN_TO_MAIN_MENU); } else { auto lcd = new LobbyClientDisconnected(); lcd->clientId = c->connectionID; boost::unique_lock lock(*mx); packsForLobbyScreen.push_back(lcd); } } } catch(...) { handleException(); throw; } } void CServerHandler::threadRunServer() { #ifndef VCMI_ANDROID setThreadName("CServerHandler::threadRunServer"); const std::string logName = (VCMIDirs::get().userCachePath() / "server_log.txt").string(); std::string comm = VCMIDirs::get().serverPath().string() + " --port=" + getDefaultPortStr() + " --run-by-client" + " --uuid=" + uuid; if(shm) { comm += " --enable-shm"; if(settings["session"]["enable-shm-uuid"].Bool()) comm += " --enable-shm-uuid"; } comm += " > \"" + logName + '\"'; int result = std::system(comm.c_str()); if (result == 0) { logNetwork->info("Server closed correctly"); } else { logNetwork->error("Error: server failed to close correctly or crashed!"); logNetwork->error("Check %s for more info", logName); } threadRunLocalServer.reset(); #endif } void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const { if(state != EClientState::STARTING) c->sendPack(&pack); } vcmi-0.99+git20190113.f06c8a87/client/CServerHandler.h000066400000000000000000000114671342332007200214610ustar00rootroot00000000000000/* * CServerHandler.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../lib/CStopWatch.h" #include "../lib/StartInfo.h" struct SharedMemory; class CConnection; class PlayerColor; struct StartInfo; class CMapInfo; struct ClientPlayer; struct CPack; struct CPackForLobby; class CClient; template class CApplier; class CBaseForLobbyApply; // TODO: Add mutex so we can't set CONNECTION_CANCELLED if client already connected, but thread not setup yet enum class EClientState : ui8 { NONE = 0, CONNECTING, // Trying to connect to server CONNECTION_CANCELLED, // Connection cancelled by player, stop attempts to connect LOBBY, // Client is connected to lobby LOBBY_CAMPAIGN, // Client is on scenario bonus selection screen STARTING, // Gameplay interfaces being created, we pause netpacks retrieving GAMEPLAY, // In-game, used by some UI DISCONNECTING // We disconnecting, drop all netpacks }; class IServerAPI { protected: virtual void sendLobbyPack(const CPackForLobby & pack) const = 0; public: virtual ~IServerAPI() {} virtual void sendClientConnecting() const = 0; virtual void sendClientDisconnecting() = 0; virtual void setCampaignState(std::shared_ptr newCampaign) = 0; virtual void setCampaignMap(int mapId) const = 0; virtual void setCampaignBonus(int bonusId) const = 0; virtual void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const = 0; virtual void setPlayer(PlayerColor color) const = 0; virtual void setPlayerOption(ui8 what, ui8 dir, PlayerColor player) const = 0; virtual void setDifficulty(int to) const = 0; virtual void setTurnLength(int npos) const = 0; virtual void sendMessage(const std::string & txt) const = 0; virtual void sendGuiAction(ui8 action) const = 0; // TODO: possibly get rid of it? virtual void sendStartGame(bool allowOnlyAI = false) const = 0; }; /// structure to handle running server and connecting to it class CServerHandler : public IServerAPI, public LobbyInfo { std::shared_ptr> applier; std::shared_ptr mx; std::list packsForLobbyScreen; //protected by mx std::vector myNames; void threadHandleConnection(); void threadRunServer(); void sendLobbyPack(const CPackForLobby & pack) const override; public: std::atomic state; //////////////////// // FIXME: Bunch of crutches to glue it all together // For starting non-custom campaign and continue to next mission std::shared_ptr campaignStateToSend; ui8 screenType; // To create lobby UI only after server is setup ui8 loadMode; // For saves filtering in SelectionTab //////////////////// std::unique_ptr th; std::shared_ptr threadRunLocalServer; std::shared_ptr c; CClient * client; CServerHandler(); void resetStateForLobby(const StartInfo::EMode mode, const std::vector * names = nullptr); void startLocalServerAndConnect(); void justConnectToServer(const std::string &addr = "", const ui16 port = 0); void applyPacksOnLobbyScreen(); void stopServerConnection(); // Helpers for lobby state access std::set getHumanColors(); PlayerColor myFirstColor() const; bool isMyColor(PlayerColor color) const; ui8 myFirstId() const; // Used by chat only! bool isServerLocal() const; bool isHost() const; bool isGuest() const; static ui16 getDefaultPort(); static std::string getDefaultPortStr(); // Lobby server API for UI void sendClientConnecting() const override; void sendClientDisconnecting() override; void setCampaignState(std::shared_ptr newCampaign) override; void setCampaignMap(int mapId) const override; void setCampaignBonus(int bonusId) const override; void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const override; void setPlayer(PlayerColor color) const override; void setPlayerOption(ui8 what, ui8 dir, PlayerColor player) const override; void setDifficulty(int to) const override; void setTurnLength(int npos) const override; void sendMessage(const std::string & txt) const override; void sendGuiAction(ui8 action) const override; void sendStartGame(bool allowOnlyAI = false) const override; void startGameplay(); void endGameplay(bool closeConnection = true, bool restart = false); void startCampaignScenario(std::shared_ptr cs = {}); // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle int howManyPlayerInterfaces(); ui8 getLoadMode(); void debugStartTest(std::string filename, bool save = false); }; extern CServerHandler * CSH; vcmi-0.99+git20190113.f06c8a87/client/CVideoHandler.cpp000066400000000000000000000225251342332007200216110ustar00rootroot00000000000000/* * CVideoHandler.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include #include "CVideoHandler.h" #include "gui/CGuiHandler.h" #include "gui/SDL_Extensions.h" #include "CPlayerInterface.h" #include "../lib/filesystem/Filesystem.h" extern CGuiHandler GH; //global gui handler #ifndef DISABLE_VIDEO //reads events and returns true on key down static bool keyDown() { SDL_Event ev; while(SDL_PollEvent(&ev)) { if(ev.type == SDL_KEYDOWN || ev.type == SDL_MOUSEBUTTONDOWN) return true; } return false; } #ifdef _MSC_VER #pragma comment(lib, "avcodec.lib") #pragma comment(lib, "avutil.lib") #pragma comment(lib, "avformat.lib") #pragma comment(lib, "swscale.lib") #endif // _MSC_VER // Define a set of functions to read data static int lodRead(void* opaque, uint8_t* buf, int size) { auto video = reinterpret_cast(opaque); return video->data->read(buf, size); } static si64 lodSeek(void * opaque, si64 pos, int whence) { auto video = reinterpret_cast(opaque); if (whence & AVSEEK_SIZE) return video->data->getSize(); return video->data->seek(pos); } CVideoPlayer::CVideoPlayer() { stream = -1; format = nullptr; codecContext = nullptr; codec = nullptr; frame = nullptr; sws = nullptr; context = nullptr; texture = nullptr; dest = nullptr; destRect = genRect(0,0,0,0); pos = genRect(0,0,0,0); refreshWait = 0; refreshCount = 0; doLoop = false; // Register codecs. TODO: May be overkill. Should call a // combination of av_register_input_format() / // av_register_output_format() / av_register_protocol() instead. av_register_all(); } bool CVideoPlayer::open(std::string fname, bool scale) { return open(fname, true, false); } // loop = to loop through the video // useOverlay = directly write to the screen. bool CVideoPlayer::open(std::string fname, bool loop, bool useOverlay, bool scale) { close(); this->fname = fname; refreshWait = 3; refreshCount = -1; doLoop = loop; ResourceID resource(std::string("Video/") + fname, EResType::VIDEO); if (!CResourceHandler::get()->existsResource(resource)) { logGlobal->error("Error: video %s was not found", resource.getName()); return false; } data = CResourceHandler::get()->load(resource); static const int BUFFER_SIZE = 4096; unsigned char * buffer = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek); format = avformat_alloc_context(); format->pb = context; // filename is not needed - file was already open and stored in this->data; int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr); if (avfopen != 0) { return false; } // Retrieve stream information if (avformat_find_stream_info(format, nullptr) < 0) return false; // Find the first video stream stream = -1; for(ui32 i=0; inb_streams; i++) { if (format->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) { stream = i; break; } } if (stream < 0) // No video stream in that file return false; // Get a pointer to the codec context for the video stream codecContext = format->streams[stream]->codec; // Find the decoder for the video stream codec = avcodec_find_decoder(codecContext->codec_id); if (codec == nullptr) { // Unsupported codec return false; } // Open codec if ( avcodec_open2(codecContext, codec, nullptr) < 0 ) { // Could not open codec codec = nullptr; return false; } // Allocate video frame frame = av_frame_alloc(); //setup scaling if(scale) { pos.w = screen->w; pos.h = screen->h; } else { pos.w = codecContext->width; pos.h = codecContext->height; } // Allocate a place to put our YUV image on that screen if (useOverlay) { texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h); } else { dest = CSDL_Ext::newSurface(pos.w, pos.h); destRect.x = destRect.y = 0; destRect.w = pos.w; destRect.h = pos.h; } if (texture == nullptr && dest == nullptr) return false; if (texture) { // Convert the image into YUV format that SDL uses sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, pos.w, pos.h, AV_PIX_FMT_YUV420P, SWS_BICUBIC, nullptr, nullptr, nullptr); } else { AVPixelFormat screenFormat = AV_PIX_FMT_NONE; if (screen->format->Bshift > screen->format->Rshift) { // this a BGR surface switch (screen->format->BytesPerPixel) { case 2: screenFormat = AV_PIX_FMT_BGR565; break; case 3: screenFormat = AV_PIX_FMT_BGR24; break; case 4: screenFormat = AV_PIX_FMT_BGR32; break; default: return false; } } else { // this a RGB surface switch (screen->format->BytesPerPixel) { case 2: screenFormat = AV_PIX_FMT_RGB565; break; case 3: screenFormat = AV_PIX_FMT_RGB24; break; case 4: screenFormat = AV_PIX_FMT_RGB32; break; default: return false; } } sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, pos.w, pos.h, screenFormat, SWS_BICUBIC, nullptr, nullptr, nullptr); } if (sws == nullptr) return false; return true; } // Read the next frame. Return false on error/end of file. bool CVideoPlayer::nextFrame() { AVPacket packet; int frameFinished = 0; bool gotError = false; if (sws == nullptr) return false; while(!frameFinished) { int ret = av_read_frame(format, &packet); if (ret < 0) { // Error. It's probably an end of file. if (doLoop && !gotError) { // Rewind if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0) break; gotError = true; } else { break; } } else { // Is this a packet from the video stream? if (packet.stream_index == stream) { // Decode video frame avcodec_decode_video2(codecContext, frame, &frameFinished, &packet); // Did we get a video frame? if (frameFinished) { AVPicture pict; if (texture) { avpicture_alloc(&pict, AV_PIX_FMT_YUV420P, pos.w, pos.h); sws_scale(sws, frame->data, frame->linesize, 0, codecContext->height, pict.data, pict.linesize); SDL_UpdateYUVTexture(texture, NULL, pict.data[0], pict.linesize[0], pict.data[1], pict.linesize[1], pict.data[2], pict.linesize[2]); avpicture_free(&pict); } else { pict.data[0] = (ui8 *)dest->pixels; pict.linesize[0] = dest->pitch; sws_scale(sws, frame->data, frame->linesize, 0, codecContext->height, pict.data, pict.linesize); } } } av_free_packet(&packet); } } return frameFinished != 0; } void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update ) { if (sws == nullptr) return; pos.x = x; pos.y = y; CSDL_Ext::blitSurface(dest, &destRect, dst, &pos); if (update) SDL_UpdateRect(dst, pos.x, pos.y, pos.w, pos.h); } void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update ) { show(x, y, dst, update); } void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update ) { if (sws == nullptr) return; if (refreshCount <= 0) { refreshCount = refreshWait; if (nextFrame()) show(x,y,dst,update); else { open(fname); nextFrame(); // The y position is wrong at the first frame. // Note: either the windows player or the linux player is // broken. Compensate here until the bug is found. show(x, y--, dst, update); } } else { redraw(x, y, dst, update); } refreshCount --; } void CVideoPlayer::close() { fname = ""; if (sws) { sws_freeContext(sws); sws = nullptr; } if (texture) { SDL_DestroyTexture(texture); texture = nullptr; } if (dest) { SDL_FreeSurface(dest); dest = nullptr; } if (frame) { av_frame_free(&frame);//will be set to null } if (codec) { avcodec_close(codecContext); codec = nullptr; codecContext = nullptr; } if (format) { avformat_close_input(&format); } if (context) { av_free(context); context = nullptr; } } // Plays a video. Only works for overlays. bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) { // Note: either the windows player or the linux player is // broken. Compensate here until the bug is found. y--; pos.x = x; pos.y = y; while(nextFrame()) { if(stopOnKey && keyDown()) return false; SDL_RenderCopy(mainRenderer, texture, nullptr, &pos); SDL_RenderPresent(mainRenderer); // Wait 3 frames GH.mainFPSmng->framerateDelay(); GH.mainFPSmng->framerateDelay(); GH.mainFPSmng->framerateDelay(); } return true; } bool CVideoPlayer::openAndPlayVideo(std::string name, int x, int y, bool stopOnKey, bool scale) { open(name, false, true, scale); bool ret = playVideo(x, y, stopOnKey); close(); return ret; } CVideoPlayer::~CVideoPlayer() { close(); } #endif vcmi-0.99+git20190113.f06c8a87/client/CVideoHandler.h000066400000000000000000000104111342332007200212450ustar00rootroot00000000000000/* * CVideoHandler.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once struct SDL_Surface; class IVideoPlayer { public: virtual bool open(std::string name, bool scale = false)=0; //true - succes virtual void close()=0; virtual bool nextFrame()=0; virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer virtual bool wait()=0; virtual int curFrame() const =0; virtual int frameCount() const =0; }; class IMainVideoPlayer : public IVideoPlayer { public: std::string fname; //name of current video file (empty if idle) virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true){} virtual bool openAndPlayVideo(std::string name, int x, int y, bool stopOnKey = false, bool scale = false) { return false; } }; class CEmptyVideoPlayer : public IMainVideoPlayer { public: int curFrame() const override {return -1;}; int frameCount() const override {return -1;}; void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {}; void show( int x, int y, SDL_Surface *dst, bool update = true ) override {}; bool nextFrame() override {return false;}; void close() override {}; bool wait() override {return false;}; bool open(std::string name, bool scale = false) override {return false;}; }; #ifndef DISABLE_VIDEO #include "../lib/filesystem/CInputStream.h" #include #include extern "C" { #include #include } //compatibility for libav 9.18 in ubuntu 14.04, 52.66.100 is ffmpeg 2.2.3 #if (LIBAVUTIL_VERSION_INT < AV_VERSION_INT(52, 66, 100)) inline AVFrame * av_frame_alloc() { return avcodec_alloc_frame(); } inline void av_frame_free(AVFrame ** frame) { av_free(*frame); *frame = nullptr; } #endif // VCMI_USE_OLD_AVUTIL //fix for travis-ci #if (LIBAVUTIL_VERSION_INT < AV_VERSION_INT(52, 0, 0)) #define AVPixelFormat PixelFormat #define AV_PIX_FMT_NONE PIX_FMT_NONE #define AV_PIX_FMT_YUV420P PIX_FMT_YUV420P #define AV_PIX_FMT_BGR565 PIX_FMT_BGR565 #define AV_PIX_FMT_BGR24 PIX_FMT_BGR24 #define AV_PIX_FMT_BGR32 PIX_FMT_BGR32 #define AV_PIX_FMT_RGB565 PIX_FMT_RGB565 #define AV_PIX_FMT_RGB24 PIX_FMT_RGB24 #define AV_PIX_FMT_RGB32 PIX_FMT_RGB32 #endif class CVideoPlayer : public IMainVideoPlayer { int stream; // stream index in video AVFormatContext *format; AVCodecContext *codecContext; // codec context for stream AVCodec *codec; AVFrame *frame; struct SwsContext *sws; AVIOContext * context; // Destination. Either overlay or dest. SDL_Texture *texture; SDL_Surface *dest; SDL_Rect destRect; // valid when dest is used SDL_Rect pos; // destination on screen int refreshWait; // Wait several refresh before updating the image int refreshCount; bool doLoop; // loop through video bool playVideo(int x, int y, bool stopOnKey); bool open(std::string fname, bool loop, bool useOverlay = false, bool scale = false); public: CVideoPlayer(); ~CVideoPlayer(); bool init(); bool open(std::string fname, bool scale = false) override; void close() override; bool nextFrame() override; // display next frame void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) bool openAndPlayVideo(std::string name, int x, int y, bool stopOnKey = false, bool scale = false) override; //TODO: bool wait() override {return false;}; int curFrame() const override {return -1;}; int frameCount() const override {return -1;}; // public to allow access from ffmpeg IO functions std::unique_ptr data; }; #endif vcmi-0.99+git20190113.f06c8a87/client/Client.cpp000066400000000000000000000470201342332007200203550ustar00rootroot00000000000000/* * Client.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Client.h" #include #include "CMusicHandler.h" #include "../lib/mapping/CCampaignHandler.h" #include "../CCallback.h" #include "../lib/CConsoleHandler.h" #include "CGameInfo.h" #include "../lib/CGameState.h" #include "CPlayerInterface.h" #include "../lib/StartInfo.h" #include "../lib/battle/BattleInfo.h" #include "../lib/CModHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/CTownHandler.h" #include "../lib/CBuildingHandler.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/serializer/CTypeList.h" #include "../lib/serializer/Connection.h" #include "../lib/serializer/CLoadIntegrityValidator.h" #include "../lib/NetPacks.h" #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapService.h" #include "../lib/JsonNode.h" #include "mapHandler.h" #include "../lib/CConfigHandler.h" #include "mainmenu/CMainMenu.h" #include "mainmenu/CCampaignScreen.h" #include "lobby/CBonusSelection.h" #include "battle/CBattleInterface.h" #include "../lib/CThreadHelper.h" #include "../lib/CScriptingModule.h" #include "../lib/registerTypes/RegisterTypes.h" #include "gui/CGuiHandler.h" #include "CMT.h" #include "CServerHandler.h" #ifdef VCMI_ANDROID #include "lib/CAndroidVMHelper.h" #endif #ifdef VCMI_ANDROID std::atomic_bool androidTestServerReadyFlag; #endif ThreadSafeVector CClient::waitingRequest; template class CApplyOnCL; class CBaseForCLApply { public: virtual void applyOnClAfter(CClient * cl, void * pack) const =0; virtual void applyOnClBefore(CClient * cl, void * pack) const =0; virtual ~CBaseForCLApply(){} template static CBaseForCLApply * getApplier(const U * t = nullptr) { return new CApplyOnCL(); } }; template class CApplyOnCL : public CBaseForCLApply { public: void applyOnClAfter(CClient * cl, void * pack) const override { T * ptr = static_cast(pack); ptr->applyCl(cl); } void applyOnClBefore(CClient * cl, void * pack) const override { T * ptr = static_cast(pack); ptr->applyFirstCl(cl); } }; template<> class CApplyOnCL: public CBaseForCLApply { public: void applyOnClAfter(CClient * cl, void * pack) const override { logGlobal->error("Cannot apply on CL plain CPack!"); assert(0); } void applyOnClBefore(CClient * cl, void * pack) const override { logGlobal->error("Cannot apply on CL plain CPack!"); assert(0); } }; CClient::CClient() { waitingRequest.clear(); applier = std::make_shared>(); registerTypesClientPacks1(*applier); registerTypesClientPacks2(*applier); IObjectInterface::cb = this; gs = nullptr; erm = nullptr; } void CClient::newGame() { CSH->th->update(); CMapService mapService; gs = new CGameState(); logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); gs->init(&mapService, CSH->si.get(), settings["general"]["saveRandomMaps"].Bool()); logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); initMapHandler(); initPlayerInterfaces(); } void CClient::loadGame() { logNetwork->info("Loading procedure started!"); std::unique_ptr loader; try { boost::filesystem::path clientSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); boost::filesystem::path controlServerSaveName; if(CResourceHandler::get("local")->existsResource(ResourceID(CSH->si->mapname, EResType::SERVER_SAVEGAME))) { controlServerSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(CSH->si->mapname, EResType::SERVER_SAVEGAME)); } else // create entry for server savegame. Triggered if save was made after launch and not yet present in res handler { controlServerSaveName = boost::filesystem::path(clientSaveName).replace_extension(".vsgm1"); CResourceHandler::get("local")->createResource(controlServerSaveName.string(), true); } if(clientSaveName.empty()) throw std::runtime_error("Cannot open client part of " + CSH->si->mapname); if(controlServerSaveName.empty() || !boost::filesystem::exists(controlServerSaveName)) throw std::runtime_error("Cannot open server part of " + CSH->si->mapname); { CLoadIntegrityValidator checkingLoader(clientSaveName, controlServerSaveName, MINIMAL_SERIALIZATION_VERSION); loadCommonState(checkingLoader); loader = checkingLoader.decay(); } } catch(std::exception & e) { logGlobal->error("Cannot load game %s. Error: %s", CSH->si->mapname, e.what()); throw; //obviously we cannot continue here } logNetwork->trace("Loaded common part of save %d ms", CSH->th->getDiff()); gs->updateOnLoad(CSH->si.get()); initMapHandler(); serialize(loader->serializer, loader->serializer.fileVersion); initPlayerInterfaces(); } void CClient::serialize(BinarySerializer & h, const int version) { assert(h.saving); ui8 players = playerint.size(); h & players; for(auto i = playerint.begin(); i != playerint.end(); i++) { logGlobal->trace("Saving player %s interface", i->first); assert(i->first == i->second->playerID); h & i->first; h & i->second->dllName; h & i->second->human; i->second->saveGame(h, version); } } void CClient::serialize(BinaryDeserializer & h, const int version) { assert(!h.saving); if(version < 787) { bool hotSeat = false; h & hotSeat; } ui8 players = 0; h & players; for(int i = 0; i < players; i++) { std::string dllname; PlayerColor pid; bool isHuman = false; h & pid; h & dllname; h & isHuman; assert(dllname.length() == 0 || !isHuman); if(pid == PlayerColor::NEUTRAL) { logGlobal->trace("Neutral battle interfaces are not serialized."); continue; } logGlobal->trace("Loading player %s interface", pid); std::shared_ptr nInt; if(dllname.length()) nInt = CDynLibHandler::getNewAI(dllname); else nInt = std::make_shared(pid); nInt->dllName = dllname; nInt->human = isHuman; nInt->playerID = pid; nInt->loadGame(h, version); // Client no longer handle this player at all if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid)) { logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid); } else if(isHuman && !vstd::contains(CSH->getHumanColors(), pid)) { logGlobal->trace("Player %s is no longer controlled by human. Destroying interface", pid); } else if(!isHuman && vstd::contains(CSH->getHumanColors(), pid)) { logGlobal->trace("Player %s is no longer controlled by AI. Destroying interface", pid); } else { installNewPlayerInterface(nInt, pid); continue; } nInt.reset(); } logNetwork->trace("Loaded client part of save %d ms", CSH->th->getDiff()); } void CClient::save(const std::string & fname) { if(gs->curB) { logNetwork->error("Game cannot be saved during battle!"); return; } SaveGame save_game(fname); sendRequest(&save_game, PlayerColor::NEUTRAL); } void CClient::endGame() { //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) for(auto & i : playerint) i.second->finish(); GH.curInt = nullptr; { boost::unique_lock un(*CPlayerInterface::pim); logNetwork->info("Ending current game!"); if(GH.topInt()) { GH.topInt()->deactivate(); } GH.listInt.clear(); GH.objsToBlit.clear(); GH.statusbar = nullptr; logNetwork->info("Removed GUI."); vstd::clear_pointer(const_cast(CGI)->mh); vstd::clear_pointer(gs); logNetwork->info("Deleted mapHandler and gameState."); LOCPLINT = nullptr; } playerint.clear(); battleints.clear(); callbacks.clear(); battleCallbacks.clear(); logNetwork->info("Deleted playerInts."); logNetwork->info("Client stopped."); } void CClient::initMapHandler() { // TODO: CMapHandler initialization can probably go somewhere else // It's can't be before initialization of interfaces // During loading CPlayerInterface from serialized state it's depend on MH if(!settings["session"]["headless"].Bool()) { const_cast(CGI)->mh = new CMapHandler(); CGI->mh->map = gs->map; logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); CGI->mh->init(); logNetwork->trace("Initializing mapHandler (together): %d ms", CSH->th->getDiff()); } pathCache.clear(); } void CClient::initPlayerInterfaces() { for(auto & elem : gs->scenarioOps->playerInfos) { PlayerColor color = elem.first; if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color)) continue; if(vstd::contains(playerint, color)) continue; logNetwork->trace("Preparing interface for player %s", color.getStr()); if(elem.second.isControlledByAI()) { auto AiToGive = aiNameForPlayer(elem.second, false); logNetwork->info("Player %s will be lead by %s", color, AiToGive); installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); } else { installNewPlayerInterface(std::make_shared(color), color); } } if(settings["session"]["spectate"].Bool()) { installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); } if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL)) installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); } std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI) { if(ps.name.size()) { const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); if(boost::filesystem::exists(aiPath)) return ps.name; } return aiNameForPlayer(battleAI); } std::string CClient::aiNameForPlayer(bool battleAI) { const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; std::string goodAI = battleAI ? settings["server"]["neutralAI"].String() : settings["server"]["playerAI"].String(); std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; //TODO what about human players if(battleints.size() >= sensibleAILimit) return badAI; return goodAI; } void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, boost::optional color, bool battlecb) { boost::unique_lock un(*CPlayerInterface::pim); PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE); if(!color) privilegedGameEventReceivers.push_back(gameInterface); playerint[colorUsed] = gameInterface; logGlobal->trace("\tInitializing the interface for player %s", colorUsed); auto cb = std::make_shared(gs, color, this); callbacks[colorUsed] = cb; battleCallbacks[colorUsed] = cb; gameInterface->init(cb); installNewBattleInterface(gameInterface, color, battlecb); } void CClient::installNewBattleInterface(std::shared_ptr battleInterface, boost::optional color, bool needCallback) { boost::unique_lock un(*CPlayerInterface::pim); PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE); if(!color) privilegedBattleEventReceivers.push_back(battleInterface); battleints[colorUsed] = battleInterface; if(needCallback) { logGlobal->trace("\tInitializing the battle interface for player %s", *color); auto cbc = std::make_shared(color, this); battleCallbacks[colorUsed] = cbc; battleInterface->init(cbc); } } void CClient::handlePack(CPack * pack) { CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier if(apply) { boost::unique_lock guiLock(*CPlayerInterface::pim); apply->applyOnClBefore(this, pack); logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name()); gs->apply(pack); logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name()); apply->applyOnClAfter(this, pack); logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name()); } else { logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name()); } delete pack; } void CClient::commitPackage(CPackForClient * pack) { CommitPackage cp; cp.freePack = false; cp.packToCommit = pack; sendRequest(&cp, PlayerColor::NEUTRAL); } int CClient::sendRequest(const CPackForServer * request, PlayerColor player) { static ui32 requestCounter = 0; ui32 requestID = requestCounter++; logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); waitingRequest.pushBack(requestID); request->requestID = requestID; request->player = player; CSH->c->sendPack(request); if(vstd::contains(playerint, player)) playerint[player]->requestSent(request, requestID); return requestID; } void CClient::battleStarted(const BattleInfo * info) { for(auto & battleCb : battleCallbacks) { if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) || battleCb.first >= PlayerColor::PLAYER_LIMIT) { battleCb.second->setBattle(info); } } // for(ui8 side : info->sides) // if(battleCallbacks.count(side)) // battleCallbacks[side]->setBattle(info); std::shared_ptr att, def; auto & leftSide = info->sides[0], & rightSide = info->sides[1]; //If quick combat is not, do not prepare interfaces for battleint if(!settings["adventure"]["quickCombat"].Bool()) { if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) att = std::dynamic_pointer_cast(playerint[leftSide.color]); if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) def = std::dynamic_pointer_cast(playerint[rightSide.color]); } if(!settings["session"]["headless"].Bool()) { Rect battleIntRect((screen->w - 800)/2, (screen->h - 600)/2, 800, 600); if(!!att || !!def) { boost::unique_lock un(*CPlayerInterface::pim); GH.pushIntT(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, battleIntRect, att, def); } else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) { //TODO: This certainly need improvement auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); spectratorInt->cb->setBattle(info); boost::unique_lock un(*CPlayerInterface::pim); GH.pushIntT(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, battleIntRect, att, def, spectratorInt); } } auto callBattleStart = [&](PlayerColor color, ui8 side) { if(vstd::contains(battleints, color)) battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side); }; callBattleStart(leftSide.color, 0); callBattleStart(rightSide.color, 1); callBattleStart(PlayerColor::UNFLAGGABLE, 1); if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) callBattleStart(PlayerColor::SPECTATOR, 1); if(info->tacticDistance && vstd::contains(battleints, info->sides[info->tacticsSide].color)) { boost::thread(&CClient::commenceTacticPhaseForInt, this, battleints[info->sides[info->tacticsSide].color]); } } void CClient::commenceTacticPhaseForInt(std::shared_ptr battleInt) { setThreadName("CClient::commenceTacticPhaseForInt"); try { battleInt->yourTacticPhase(gs->curB->tacticDistance); if(gs && !!gs->curB && gs->curB->tacticDistance) //while awaiting for end of tactics phase, many things can happen (end of battle... or game) { MakeAction ma(BattleAction::makeEndOFTacticPhase(gs->curB->playerToSide(battleInt->playerID).get())); sendRequest(&ma, battleInt->playerID); } } catch(...) { handleException(); } } void CClient::battleFinished() { stopAllBattleActions(); for(auto & side : gs->curB->sides) if(battleCallbacks.count(side.color)) battleCallbacks[side.color]->setBattle(nullptr); if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr); } void CClient::startPlayerBattleAction(PlayerColor color) { stopPlayerBattleAction(color); if(vstd::contains(battleints, color)) { auto thread = std::make_shared(std::bind(&CClient::waitForMoveAndSend, this, color)); playerActionThreads[color] = thread; } } void CClient::stopPlayerBattleAction(PlayerColor color) { if(vstd::contains(playerActionThreads, color)) { auto thread = playerActionThreads.at(color); if(thread->joinable()) { thread->interrupt(); thread->join(); } playerActionThreads.erase(color); } } void CClient::stopAllBattleActions() { while(!playerActionThreads.empty()) stopPlayerBattleAction(playerActionThreads.begin()->first); } void CClient::waitForMoveAndSend(PlayerColor color) { try { setThreadName("CClient::waitForMoveAndSend"); assert(vstd::contains(battleints, color)); BattleAction ba = battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false)); if(ba.actionType != EActionType::CANCEL) { logNetwork->trace("Send battle action to server: %s", ba.toString()); MakeAction temp_action(ba); sendRequest(&temp_action, color); } } catch(boost::thread_interrupted &) { logNetwork->debug("Wait for move thread was interrupted and no action will be send. Was a battle ended by spell?"); } catch(...) { handleException(); } } void CClient::invalidatePaths() { boost::unique_lock pathLock(pathCacheMutex); pathCache.clear(); } std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) { assert(h); boost::unique_lock pathLock(pathCacheMutex); auto iter = pathCache.find(h); if(iter == std::end(pathCache)) { std::shared_ptr paths = std::make_shared(getMapSize(), h); gs->calculatePaths(h, *paths.get()); pathCache[h] = paths; return paths; } else { return iter->second; } } PlayerColor CClient::getLocalPlayer() const { if(LOCPLINT) return LOCPLINT->playerID; return getCurrentPlayer(); } #ifdef VCMI_ANDROID extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerReady(JNIEnv * env, jobject cls) { logNetwork->info("Received server ready signal"); androidTestServerReadyFlag.store(true); } extern "C" JNIEXPORT bool JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jobject cls) { logGlobal->info("Received emergency save game request"); if(!LOCPLINT || !LOCPLINT->cb) { return false; } LOCPLINT->cb->save("Saves/_Android_Autosave"); return true; } #endif vcmi-0.99+git20190113.f06c8a87/client/Client.h000066400000000000000000000225631342332007200200270ustar00rootroot00000000000000/* * Client.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../lib/IGameCallback.h" #include "../lib/battle/BattleAction.h" #include "../lib/CStopWatch.h" #include "../lib/int3.h" #include "../lib/CondSh.h" #include "../lib/CPathfinder.h" struct CPack; struct CPackForServer; class CCampaignState; class CBattleCallback; class IGameEventsReceiver; class IBattleEventsReceiver; class CBattleGameInterface; class CGameState; class CGameInterface; class CCallback; struct BattleAction; class CClient; class CScriptingModule; struct CPathsInfo; class BinaryDeserializer; class BinarySerializer; namespace boost { class thread; } template class CApplier; class CBaseForCLApply; template class ThreadSafeVector { typedef std::vector TVector; typedef boost::unique_lock TLock; TVector items; boost::mutex mx; boost::condition_variable cond; public: void clear() { TLock lock(mx); items.clear(); cond.notify_all(); } void pushBack(const T & item) { TLock lock(mx); items.push_back(item); cond.notify_all(); } // //to access list, caller must present a lock used to lock mx // TVector &getList(TLock &lockedLock) // { // assert(lockedLock.owns_lock() && lockedLock.mutex() == &mx); // return items; // } TLock getLock() { return TLock(mx); } void waitWhileContains(const T & item) { auto lock = getLock(); while(vstd::contains(items, item)) cond.wait(lock); } bool tryRemovingElement(const T & item) //returns false if element was not present { auto lock = getLock(); auto itr = vstd::find(items, item); if(itr == items.end()) //not in container { return false; } items.erase(itr); cond.notify_all(); return true; } }; /// Class which handles client - server logic class CClient : public IGameCallback { std::shared_ptr> applier; mutable boost::mutex pathCacheMutex; std::map> pathCache; std::map> playerActionThreads; void waitForMoveAndSend(PlayerColor color); public: std::map> callbacks; //callbacks given to player interfaces std::map> battleCallbacks; //callbacks given to player interfaces std::vector> privilegedGameEventReceivers; //scripting modules, spectator interfaces std::vector> privilegedBattleEventReceivers; //scripting modules, spectator interfaces std::map> playerint; std::map> battleints; std::map>> additionalPlayerInts; std::map>> additionalBattleInts; boost::optional curbaction; CScriptingModule * erm; CClient(); void newGame(); void loadGame(); void serialize(BinarySerializer & h, const int version); void serialize(BinaryDeserializer & h, const int version); void save(const std::string & fname); void endGame(); void initMapHandler(); void initPlayerInterfaces(); std::string aiNameForPlayer(const PlayerSettings & ps, bool battleAI); //empty means no AI -> human std::string aiNameForPlayer(bool battleAI); void installNewPlayerInterface(std::shared_ptr gameInterface, boost::optional color, bool battlecb = false); void installNewBattleInterface(std::shared_ptr battleInterface, boost::optional color, bool needCallback = true); static ThreadSafeVector waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction) void handlePack(CPack * pack); //applies the given pack and deletes it void commitPackage(CPackForClient * pack) override; int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request void battleStarted(const BattleInfo * info); void commenceTacticPhaseForInt(std::shared_ptr battleInt); //will be called as separate thread void battleFinished(); void startPlayerBattleAction(PlayerColor color); void stopPlayerBattleAction(PlayerColor color); void stopAllBattleActions(); void invalidatePaths(); std::shared_ptr getPathsInfo(const CGHeroInstance * h); virtual PlayerColor getLocalPlayer() const override; friend class CCallback; //handling players actions friend class CBattleCallback; //handling players actions void changeSpells(const CGHeroInstance * hero, bool give, const std::set & spells) override {}; bool removeObject(const CGObjectInstance * obj) override {return false;}; void setBlockVis(ObjectInstanceID objid, bool bv) override {}; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs = false) override {}; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; void showBlockingDialog(BlockingDialog * iw) override {}; void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {}; void showTeleportDialog(TeleportDialog * iw) override {}; void showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) override {}; void giveResource(PlayerColor player, Res::ERes which, int val) override {}; virtual void giveResources(PlayerColor player, TResources resources) override {}; void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {}; void takeCreatures(ObjectInstanceID objid, const std::vector & creatures) override {}; bool changeStackType(const StackLocation & sl, const CCreature * c) override {return false;}; bool changeStackCount(const StackLocation & sl, TQuantity count, bool absoluteValue = false) override {return false;}; bool insertNewStack(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;}; bool eraseStack(const StackLocation & sl, bool forceRemoval = false) override {return false;}; bool swapStacks(const StackLocation & sl1, const StackLocation & sl2) override {return false;} bool addToSlot(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;} void tryJoiningArmy(const CArmedInstance * src, const CArmedInstance * dst, bool removeObjWhenFinished, bool allowMerging) override {} bool moveStack(const StackLocation & src, const StackLocation & dst, TQuantity count = -1) override {return false;} void removeAfterVisit(const CGObjectInstance * object) override {}; void giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {}; void giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {}; void putArtifact(const ArtifactLocation & al, const CArtifactInstance * a) override {}; void removeArtifact(const ArtifactLocation & al) override {}; bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;}; void synchronizeArtifactHandlerLists() override {}; void showCompInfo(ShowInInfobox * comp) override {}; void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle void setAmount(ObjectInstanceID objid, ui32 val) override {}; bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; void giveHeroBonus(GiveBonus * bonus) override {}; void setMovePoints(SetMovePoints * smp) override {}; void setManaPoints(ObjectInstanceID hid, int val) override {}; void giveHero(ObjectInstanceID id, PlayerColor player) override {}; void changeObjPos(ObjectInstanceID objid, int3 newPos, ui8 flags) override {}; void sendAndApply(CPackForClient * pack) override {}; void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {}; void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, bool hide) override {} }; vcmi-0.99+git20190113.f06c8a87/client/CreatureCostBox.cpp000066400000000000000000000031001342332007200222020ustar00rootroot00000000000000/* * CreatureCostBox.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CreatureCostBox.h" #include "widgets/Images.h" #include "widgets/TextControls.h" #include "gui/CGuiHandler.h" CreatureCostBox::CreatureCostBox(Rect position, std::string titleText) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); type |= REDRAW_PARENT; pos = position + pos; title = std::make_shared(pos.w/2, 10, FONT_SMALL, CENTER, Colors::WHITE, titleText); } void CreatureCostBox::set(TResources res) { for(auto & item : resources) item.second.first->setText(boost::lexical_cast(res[item.first])); } void CreatureCostBox::createItems(TResources res) { resources.clear(); OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); TResources::nziterator iter(res); while(iter.valid()) { ImagePtr image = std::make_shared("RESOURCE", iter->resType); LabelPtr text = std::make_shared(15, 43, FONT_SMALL, CENTER, Colors::WHITE, "0"); resources.insert(std::make_pair(iter->resType, std::make_pair(text, image))); iter++; } if(!resources.empty()) { int curx = pos.w / 2 - (16 * resources.size()) - (8 * (resources.size() - 1)); //reverse to display gold as first resource for(auto & currentRes : boost::adaptors::reverse(resources)) { currentRes.second.first->moveBy(Point(curx, 22)); currentRes.second.second->moveBy(Point(curx, 22)); curx += 48; } } redraw(); } vcmi-0.99+git20190113.f06c8a87/client/CreatureCostBox.h000066400000000000000000000012561342332007200216610ustar00rootroot00000000000000/* * CreatureCostBox.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../lib/ResourceSet.h" #include "gui/CIntObject.h" class CLabel; class CAnimImage; class CreatureCostBox : public CIntObject { public: void set(TResources res); CreatureCostBox(Rect position, std::string titleText); void createItems(TResources res); private: using LabelPtr = std::shared_ptr; using ImagePtr = std::shared_ptr; LabelPtr title; std::map> resources; }; vcmi-0.99+git20190113.f06c8a87/client/DPIaware.manifest000066400000000000000000000006241342332007200216160ustar00rootroot00000000000000 true vcmi-0.99+git20190113.f06c8a87/client/Graphics.cpp000066400000000000000000000333541342332007200207040ustar00rootroot00000000000000/* * Graphics.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Graphics.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/CBinaryReader.h" #include "gui/SDL_Extensions.h" #include "gui/CAnimation.h" #include #include "../lib/CThreadHelper.h" #include "CGameInfo.h" #include "../lib/VCMI_Lib.h" #include "../CCallback.h" #include "../lib/CHeroHandler.h" #include "../lib/CTownHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CCreatureHandler.h" #include "CBitmapHandler.h" #include "../lib/CSkillHandler.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/CGameState.h" #include "../lib/JsonNode.h" #include "../lib/vcmi_endian.h" #include "../lib/CStopWatch.h" #include "../lib/mapObjects/CObjectClassesHandler.h" #include "../lib/mapObjects/CObjectHandler.h" using namespace CSDL_Ext; Graphics * graphics = nullptr; void Graphics::loadPaletteAndColors() { auto textFile = CResourceHandler::get()->load(ResourceID("DATA/PLAYERS.PAL"))->readAll(); std::string pals((char*)textFile.first.get(), textFile.second); playerColorPalette = new SDL_Color[256]; neutralColor = new SDL_Color; playerColors = new SDL_Color[PlayerColor::PLAYER_LIMIT_I]; int startPoint = 24; //beginning byte; used to read for(int i=0; i<256; ++i) { SDL_Color col; col.r = pals[startPoint++]; col.g = pals[startPoint++]; col.b = pals[startPoint++]; col.a = SDL_ALPHA_OPAQUE; startPoint++; playerColorPalette[i] = col; } neutralColorPalette = new SDL_Color[32]; auto stream = CResourceHandler::get()->load(ResourceID("config/NEUTRAL.PAL")); CBinaryReader reader(stream.get()); for(int i=0; i<32; ++i) { neutralColorPalette[i].r = reader.readUInt8(); neutralColorPalette[i].g = reader.readUInt8(); neutralColorPalette[i].b = reader.readUInt8(); reader.readUInt8(); // this is "flags" entry, not alpha neutralColorPalette[i].a = SDL_ALPHA_OPAQUE; } //colors initialization SDL_Color colors[] = { {0xff,0, 0, SDL_ALPHA_OPAQUE}, {0x31,0x52,0xff,SDL_ALPHA_OPAQUE}, {0x9c,0x73,0x52,SDL_ALPHA_OPAQUE}, {0x42,0x94,0x29,SDL_ALPHA_OPAQUE}, {0xff,0x84,0, SDL_ALPHA_OPAQUE}, {0x8c,0x29,0xa5,SDL_ALPHA_OPAQUE}, {0x09,0x9c,0xa5,SDL_ALPHA_OPAQUE}, {0xc6,0x7b,0x8c,SDL_ALPHA_OPAQUE}}; for(int i=0;i<8;i++) { playerColors[i] = colors[i]; } //gray neutralColor->r = 0x84; neutralColor->g = 0x84; neutralColor->b = 0x84; neutralColor->a = SDL_ALPHA_OPAQUE; } void Graphics::initializeBattleGraphics() { const JsonNode config(ResourceID("config/battles_graphics.json")); // Reserve enough space for the terrains int idx = config["backgrounds"].Vector().size(); battleBacks.resize(idx+1); // 1 to idx, 0 is unused idx = 1; for(const JsonNode &t : config["backgrounds"].Vector()) { battleBacks[idx].push_back(t.String()); idx++; } //initialization of AC->def name mapping for(const JsonNode &ac : config["ac_mapping"].Vector()) { int ACid = ac["id"].Float(); std::vector< std::string > toAdd; for(const JsonNode &defname : ac["defnames"].Vector()) { toAdd.push_back(defname.String()); } battleACToDef[ACid] = toAdd; } } Graphics::Graphics() { #if 0 std::vector tasks; //preparing list of graphics to load tasks += std::bind(&Graphics::loadFonts,this); tasks += std::bind(&Graphics::loadPaletteAndColors,this); tasks += std::bind(&Graphics::initializeBattleGraphics,this); tasks += std::bind(&Graphics::loadErmuToPicture,this); tasks += std::bind(&Graphics::initializeImageLists,this); CThreadHelper th(&tasks,std::max((ui32)1,boost::thread::hardware_concurrency())); th.run(); #else loadFonts(); loadPaletteAndColors(); initializeBattleGraphics(); loadErmuToPicture(); initializeImageLists(); #endif //(!) do not load any CAnimation here } Graphics::~Graphics() { delete[] playerColors; delete neutralColor; delete[] playerColorPalette; delete[] neutralColorPalette; } void Graphics::load() { heroMoveArrows = std::make_shared("ADAG"); heroMoveArrows->preload(); loadHeroAnimations(); loadHeroFlagAnimations(); loadFogOfWar(); } void Graphics::loadHeroAnimations() { for(auto & elem : CGI->heroh->classes.heroClasses) { for (auto & templ : VLC->objtypeh->getHandlerFor(Obj::HERO, elem->id)->getTemplates()) { if (!heroAnimations.count(templ.animationFile)) heroAnimations[templ.animationFile] = loadHeroAnimation(templ.animationFile); } } boatAnimations[0] = loadHeroAnimation("AB01_.DEF"); boatAnimations[1] = loadHeroAnimation("AB02_.DEF"); boatAnimations[2] = loadHeroAnimation("AB03_.DEF"); mapObjectAnimations["AB01_.DEF"] = boatAnimations[0]; mapObjectAnimations["AB02_.DEF"] = boatAnimations[1]; mapObjectAnimations["AB03_.DEF"] = boatAnimations[2]; } void Graphics::loadHeroFlagAnimations() { static const std::vector HERO_FLAG_ANIMATIONS = { "AF00", "AF01","AF02","AF03", "AF04", "AF05","AF06","AF07" }; static const std::vector< std::vector > BOAT_FLAG_ANIMATIONS = { { "ABF01L", "ABF01G", "ABF01R", "ABF01D", "ABF01B", "ABF01P", "ABF01W", "ABF01K" }, { "ABF02L", "ABF02G", "ABF02R", "ABF02D", "ABF02B", "ABF02P", "ABF02W", "ABF02K" }, { "ABF03L", "ABF03G", "ABF03R", "ABF03D", "ABF03B", "ABF03P", "ABF03W", "ABF03K" } }; for(const auto & name : HERO_FLAG_ANIMATIONS) heroFlagAnimations.push_back(loadHeroFlagAnimation(name)); for(int i = 0; i < BOAT_FLAG_ANIMATIONS.size(); i++) for(const auto & name : BOAT_FLAG_ANIMATIONS[i]) boatFlagAnimations[i].push_back(loadHeroFlagAnimation(name)); } std::shared_ptr Graphics::loadHeroFlagAnimation(const std::string & name) { //first - group number to be rotated, second - group number after rotation static const std::vector > rotations = { {6,10}, {7,11}, {8,12}, {1,13}, {2,14}, {3,15} }; std::shared_ptr anim = std::make_shared(name); anim->preload(); for(const auto & rotation : rotations) { const int sourceGroup = rotation.first; const int targetGroup = rotation.second; anim->createFlippedGroup(sourceGroup, targetGroup); } return anim; } std::shared_ptr Graphics::loadHeroAnimation(const std::string &name) { //first - group number to be rotated, second - group number after rotation static const std::vector > rotations = { {6,10}, {7,11}, {8,12}, {1,13}, {2,14}, {3,15} }; std::shared_ptr anim = std::make_shared(name); anim->preload(); for(const auto & rotation : rotations) { const int sourceGroup = rotation.first; const int targetGroup = rotation.second; anim->createFlippedGroup(sourceGroup, targetGroup); } return anim; } void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) { if(sur->format->palette) { SDL_Color * palette = nullptr; if(player < PlayerColor::PLAYER_LIMIT) { palette = playerColorPalette + 32*player.getNum(); } else if(player == PlayerColor::NEUTRAL) { palette = neutralColorPalette; } else { logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.getStr()); return; } //FIXME: not all player colored images have player palette at last 32 indexes //NOTE: following code is much more correct but still not perfect (bugged with status bar) SDL_SetColors(sur, palette, 224, 32); #if 0 SDL_Color * bluePalette = playerColorPalette + 32; SDL_Palette * oldPalette = sur->format->palette; SDL_Palette * newPalette = SDL_AllocPalette(256); for(size_t destIndex = 0; destIndex < 256; destIndex++) { SDL_Color old = oldPalette->colors[destIndex]; bool found = false; for(size_t srcIndex = 0; srcIndex < 32; srcIndex++) { if(old.b == bluePalette[srcIndex].b && old.g == bluePalette[srcIndex].g && old.r == bluePalette[srcIndex].r) { found = true; newPalette->colors[destIndex] = palette[srcIndex]; break; } } if(!found) newPalette->colors[destIndex] = old; } SDL_SetSurfacePalette(sur, newPalette); SDL_FreePalette(newPalette); #endif // 0 } else { //TODO: implement. H3 method works only for images with palettes. // Add some kind of player-colored overlay? // Or keep palette approach here and replace only colors of specific value(s) // Or just wait for OpenGL support? logGlobal->warn("Image must have palette to be player-colored!"); } } void Graphics::loadFogOfWar() { fogOfWarFullHide = std::make_shared("TSHRC"); fogOfWarFullHide->preload(); fogOfWarPartialHide = std::make_shared("TSHRE"); fogOfWarPartialHide->preload(); static const int rotations [] = {22, 15, 2, 13, 12, 16, 28, 17, 20, 19, 7, 24, 26, 25, 30, 32, 27}; size_t size = fogOfWarPartialHide->size(0);//group size after next rotation for(const int rotation : rotations) { fogOfWarPartialHide->duplicateImage(0, rotation, 0); auto image = fogOfWarPartialHide->getImage(size, 0); image->verticalFlip(); size++; } } void Graphics::loadFonts() { const JsonNode config(ResourceID("config/fonts.json")); const JsonVector & bmpConf = config["bitmap"].Vector(); const JsonNode & ttfConf = config["trueType"]; const JsonNode & hanConf = config["bitmapHan"]; assert(bmpConf.size() == FONTS_NUMBER); for (size_t i=0; i(hanConf[filename]); else if (!ttfConf[filename].isNull()) // no ttf override fonts[i] = std::make_shared(ttfConf[filename]); else fonts[i] = std::make_shared(filename); } } std::shared_ptr Graphics::getAnimation(const CGObjectInstance* obj) { return getAnimation(obj->appearance); } std::shared_ptr Graphics::getAnimation(const ObjectTemplate & info) { //the only(?) invisible object if(info.id == Obj::EVENT) { return std::shared_ptr(); } if(info.animationFile.empty()) { logGlobal->warn("Def name for obj (%d,%d) is empty!", info.id, info.subid); return std::shared_ptr(); } std::shared_ptr ret = mapObjectAnimations[info.animationFile]; //already loaded if(ret) { ret->preload(); return ret; } ret = std::make_shared(info.animationFile); mapObjectAnimations[info.animationFile] = ret; ret->preload(); return ret; } void Graphics::loadErmuToPicture() { //loading ERMU to picture const JsonNode config(ResourceID("config/ERMU_to_picture.json")); int etp_idx = 0; for(const JsonNode &etp : config["ERMU_to_picture"].Vector()) { int idx = 0; for(const JsonNode &n : etp.Vector()) { ERMUtoPicture[idx][etp_idx] = n.String(); idx ++; } assert (idx == ARRAY_COUNT(ERMUtoPicture)); etp_idx ++; } assert (etp_idx == 44); } void Graphics::addImageListEntry(size_t index, std::string listName, std::string imageName) { if (!imageName.empty()) { JsonNode entry; entry["frame"].Float() = index; entry["file"].String() = imageName; imageLists["SPRITES/" + listName]["images"].Vector().push_back(entry); } } void Graphics::initializeImageLists() { for(const CCreature * creature : CGI->creh->creatures) { addImageListEntry(creature->iconIndex, "CPRSMALL", creature->smallIconName); addImageListEntry(creature->iconIndex, "TWCRPORT", creature->largeIconName); } for(const CHero * hero : CGI->heroh->heroes) { addImageListEntry(hero->imageIndex, "UN32", hero->iconSpecSmall); addImageListEntry(hero->imageIndex, "UN44", hero->iconSpecLarge); addImageListEntry(hero->imageIndex, "PORTRAITSLARGE", hero->portraitLarge); addImageListEntry(hero->imageIndex, "PORTRAITSSMALL", hero->portraitSmall); } for(const CArtifact * art : CGI->arth->artifacts) { addImageListEntry(art->iconIndex, "ARTIFACT", art->image); addImageListEntry(art->iconIndex, "ARTIFACTLARGE", art->large); } for(const CFaction * faction : CGI->townh->factions) { if (faction->town) { auto & info = faction->town->clientInfo; addImageListEntry(info.icons[0][0], "ITPT", info.iconLarge[0][0]); addImageListEntry(info.icons[0][1], "ITPT", info.iconLarge[0][1]); addImageListEntry(info.icons[1][0], "ITPT", info.iconLarge[1][0]); addImageListEntry(info.icons[1][1], "ITPT", info.iconLarge[1][1]); addImageListEntry(info.icons[0][0] + 2, "ITPA", info.iconSmall[0][0]); addImageListEntry(info.icons[0][1] + 2, "ITPA", info.iconSmall[0][1]); addImageListEntry(info.icons[1][0] + 2, "ITPA", info.iconSmall[1][0]); addImageListEntry(info.icons[1][1] + 2, "ITPA", info.iconSmall[1][1]); } } for(const CSpell * spell : CGI->spellh->objects) { addImageListEntry(spell->id, "SPELLS", spell->iconBook); addImageListEntry(spell->id+1, "SPELLINT", spell->iconEffect); addImageListEntry(spell->id, "SPELLBON", spell->iconScenarioBonus); addImageListEntry(spell->id, "SPELLSCR", spell->iconScroll); } for(const CSkill * skill : CGI->skillh->objects) { for(int level = 1; level <= 3; level++) { int frame = 2 + level + 3 * skill->id; const CSkill::LevelInfo & skillAtLevel = skill->at(level); addImageListEntry(frame, "SECSK32", skillAtLevel.iconSmall); addImageListEntry(frame, "SECSKILL", skillAtLevel.iconMedium); addImageListEntry(frame, "SECSK82", skillAtLevel.iconLarge); } } } vcmi-0.99+git20190113.f06c8a87/client/Graphics.h000066400000000000000000000064641342332007200203530ustar00rootroot00000000000000/* * Graphics.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "gui/Fonts.h" #include "../lib/GameConstants.h" #include "gui/Geometries.h" struct SDL_Surface; class CGHeroInstance; class CGTownInstance; class CHeroClass; struct SDL_Color; struct InfoAboutHero; struct InfoAboutTown; class CGObjectInstance; class ObjectTemplate; class CAnimation; enum EFonts { FONT_BIG, FONT_CALLI, FONT_CREDITS, FONT_HIGH_SCORE, FONT_MEDIUM, FONT_SMALL, FONT_TIMES, FONT_TINY, FONT_VERD }; /// Handles fonts, hero images, town images, various graphics class Graphics { void addImageListEntry(size_t index, std::string listName, std::string imageName); void initializeBattleGraphics(); void loadPaletteAndColors(); void loadHeroAnimations(); //loads animation and adds required rotated frames std::shared_ptr loadHeroAnimation(const std::string &name); void loadHeroFlagAnimations(); //loads animation and adds required rotated frames std::shared_ptr loadHeroFlagAnimation(const std::string &name); void loadErmuToPicture(); void loadFogOfWar(); void loadFonts(); void initializeImageLists(); public: //Fonts static const int FONTS_NUMBER = 9; std::array< std::shared_ptr, FONTS_NUMBER> fonts; //various graphics SDL_Color * playerColors; //array [8] SDL_Color * neutralColor; SDL_Color * playerColorPalette; //palette to make interface colors good - array of size [256] SDL_Color * neutralColorPalette; std::shared_ptr heroMoveArrows; // [hero class def name] //added group 10: up - left, 11 - left and 12 - left down // 13 - up-left standing; 14 - left standing; 15 - left down standing std::map< std::string, std::shared_ptr > heroAnimations; std::vector< std::shared_ptr > heroFlagAnimations; // [boat type: 0 .. 2] //added group 10: up - left, 11 - left and 12 - left down // 13 - up-left standing; 14 - left standing; 15 - left down standing std::array< std::shared_ptr, 3> boatAnimations; std::array< std::vector >, 3> boatFlagAnimations; //all other objects (not hero or boat) std::map< std::string, std::shared_ptr > mapObjectAnimations; std::shared_ptr fogOfWarFullHide; std::shared_ptr fogOfWarPartialHide; std::map imageLists; //towns std::map ERMUtoPicture[GameConstants::F_NUMBER]; //maps building ID to it's picture's name for each town type //for battles std::vector< std::vector< std::string > > battleBacks; //battleBacks[terType] - vector of possible names for certain terrain type std::map< int, std::vector < std::string > > battleACToDef; //maps AC format to vector of appropriate def names //functions Graphics(); ~Graphics(); void load(); void blueToPlayersAdv(SDL_Surface * sur, PlayerColor player); //replaces blue interface colour with a color of player std::shared_ptr getAnimation(const CGObjectInstance * obj); std::shared_ptr getAnimation(const ObjectTemplate & info); }; extern Graphics * graphics; vcmi-0.99+git20190113.f06c8a87/client/NetPacksClient.cpp000066400000000000000000000705001342332007200220050ustar00rootroot00000000000000/* * NetPacksClient.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "../lib/NetPacks.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/FileInfo.h" #include "../CCallback.h" #include "Client.h" #include "CPlayerInterface.h" #include "CGameInfo.h" #include "../lib/serializer/Connection.h" #include "../lib/serializer/BinarySerializer.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/VCMI_Lib.h" #include "../lib/mapping/CMap.h" #include "../lib/VCMIDirs.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/CSoundBase.h" #include "../lib/StartInfo.h" #include "mapHandler.h" #include "windows/GUIClasses.h" #include "../lib/CConfigHandler.h" #include "gui/SDL_Extensions.h" #include "battle/CBattleInterface.h" #include "../lib/mapping/CCampaignHandler.h" #include "../lib/CGameState.h" #include "../lib/CStack.h" #include "../lib/battle/BattleInfo.h" #include "../lib/GameConstants.h" #include "../lib/CPlayerState.h" #include "gui/CGuiHandler.h" #include "widgets/MiscWidgets.h" #include "widgets/AdventureMapClasses.h" #include "CMT.h" #include "CServerHandler.h" // TODO: as Tow suggested these template should all be part of CClient // This will require rework spectator interface properly though template void callPrivilegedInterfaces(CClient * cl, void (T::*ptr)(Args...), Args2 && ...args) { for(auto &ger : cl->privilegedGameEventReceivers) ((*ger).*ptr)(std::forward(args)...); } template bool callOnlyThatInterface(CClient * cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) { if(vstd::contains(cl->playerint, player)) { ((*cl->playerint[player]).*ptr)(std::forward(args)...); return true; } return false; } template bool callInterfaceIfPresent(CClient * cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) { bool called = callOnlyThatInterface(cl, player, ptr, std::forward(args)...); callPrivilegedInterfaces(cl, ptr, std::forward(args)...); return called; } template void callOnlyThatBattleInterface(CClient * cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) { if(vstd::contains(cl->battleints,player)) ((*cl->battleints[player]).*ptr)(std::forward(args)...); if(cl->additionalBattleInts.count(player)) { for(auto bInt : cl->additionalBattleInts[player]) ((*bInt).*ptr)(std::forward(args)...); } } template void callPrivilegedBattleInterfaces(CClient * cl, void (T::*ptr)(Args...), Args2 && ...args) { for(auto & ber : cl->privilegedBattleEventReceivers) ((*ber).*ptr)(std::forward(args)...); } template void callBattleInterfaceIfPresent(CClient * cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) { callOnlyThatInterface(cl, player, ptr, std::forward(args)...); callPrivilegedBattleInterfaces(cl, ptr, std::forward(args)...); } //calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy template void callAllInterfaces(CClient * cl, void (T::*ptr)(Args...), Args2 && ...args) { for(auto pInt : cl->playerint) ((*pInt.second).*ptr)(std::forward(args)...); } //calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy template void callBattleInterfaceIfPresentForBothSides(CClient * cl, void (T::*ptr)(Args...), Args2 && ...args) { callOnlyThatBattleInterface(cl, cl->gameState()->curB->sides[0].color, ptr, std::forward(args)...); callOnlyThatBattleInterface(cl, cl->gameState()->curB->sides[1].color, ptr, std::forward(args)...); if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt) { callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward(args)...); } callPrivilegedBattleInterfaces(cl, ptr, std::forward(args)...); } void SetResources::applyCl(CClient *cl) { //todo: inform on actual resource set transfered callInterfaceIfPresent(cl, player, &IGameEventsReceiver::receivedResource); } void SetPrimSkill::applyCl(CClient *cl) { const CGHeroInstance *h = cl->getHero(id); if(!h) { logNetwork->error("Cannot find hero with ID %d", id.getNum()); return; } callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroPrimarySkillChanged, h, which, val); } void SetSecSkill::applyCl(CClient *cl) { const CGHeroInstance *h = cl->getHero(id); if(!h) { logNetwork->error("Cannot find hero with ID %d", id.getNum()); return; } callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroSecondarySkillChanged, h, which, val); } void HeroVisitCastle::applyCl(CClient *cl) { const CGHeroInstance *h = cl->getHero(hid); if(start()) { callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroVisitsTown, h, GS(cl)->getTown(tid)); } } void ChangeSpells::applyCl(CClient *cl) { //TODO: inform interface? } void SetMana::applyCl(CClient *cl) { const CGHeroInstance *h = cl->getHero(hid); callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h); } void SetMovePoints::applyCl(CClient *cl) { const CGHeroInstance *h = cl->getHero(hid); cl->invalidatePaths(); callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h); } void FoWChange::applyCl(CClient *cl) { for(auto &i : cl->playerint) { if(cl->getPlayerRelations(i.first, player) == PlayerRelations::SAME_PLAYER && waitForDialogs && LOCPLINT == i.second.get()) { LOCPLINT->waitWhileDialog(); } if(cl->getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES) { if(mode) i.second->tileRevealed(tiles); else i.second->tileHidden(tiles); } } cl->invalidatePaths(); } void SetAvailableHeroes::applyCl(CClient *cl) { //TODO: inform interface? } static void dispatchGarrisonChange(CClient * cl, ObjectInstanceID army1, ObjectInstanceID army2) { auto obj1 = cl->getObj(army1); if(!obj1) { logNetwork->error("Cannot find army with ID %d", army1.getNum()); return; } callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); if(army2 != ObjectInstanceID() && army2 != army1) { auto obj2 = cl->getObj(army2); if(!obj2) { logNetwork->error("Cannot find army with ID %d", army2.getNum()); return; } if(obj1->tempOwner != obj2->tempOwner) callInterfaceIfPresent(cl, obj2->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); } } void ChangeStackCount::applyCl(CClient * cl) { dispatchGarrisonChange(cl, army, ObjectInstanceID()); } void SetStackType::applyCl(CClient * cl) { dispatchGarrisonChange(cl, army, ObjectInstanceID()); } void EraseStack::applyCl(CClient * cl) { dispatchGarrisonChange(cl, army, ObjectInstanceID()); } void SwapStacks::applyCl(CClient * cl) { dispatchGarrisonChange(cl, srcArmy, dstArmy); } void InsertNewStack::applyCl(CClient * cl) { dispatchGarrisonChange(cl, army, ObjectInstanceID()); } void RebalanceStacks::applyCl(CClient * cl) { dispatchGarrisonChange(cl, srcArmy, dstArmy); } void PutArtifact::applyCl(CClient *cl) { callInterfaceIfPresent(cl, al.owningPlayer(), &IGameEventsReceiver::artifactPut, al); } void EraseArtifact::applyCl(CClient *cl) { callInterfaceIfPresent(cl, al.owningPlayer(), &IGameEventsReceiver::artifactRemoved, al); } void MoveArtifact::applyCl(CClient *cl) { callInterfaceIfPresent(cl, src.owningPlayer(), &IGameEventsReceiver::artifactMoved, src, dst); if(src.owningPlayer() != dst.owningPlayer()) callInterfaceIfPresent(cl, dst.owningPlayer(), &IGameEventsReceiver::artifactMoved, src, dst); } void AssembledArtifact::applyCl(CClient *cl) { callInterfaceIfPresent(cl, al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, al); } void DisassembledArtifact::applyCl(CClient *cl) { callInterfaceIfPresent(cl, al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, al); } void HeroVisit::applyCl(CClient * cl) { auto hero = cl->getHero(heroId); auto obj = cl->getObj(objId, false); callInterfaceIfPresent(cl, player, &IGameEventsReceiver::heroVisit, hero, obj, starting); } void NewTurn::applyCl(CClient *cl) { cl->invalidatePaths(); } void GiveBonus::applyCl(CClient *cl) { cl->invalidatePaths(); switch(who) { case HERO: { const CGHeroInstance *h = GS(cl)->getHero(ObjectInstanceID(id)); callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, *h->getBonusList().back(), true); } break; case PLAYER: { const PlayerState *p = GS(cl)->getPlayer(PlayerColor(id)); callInterfaceIfPresent(cl, PlayerColor(id), &IGameEventsReceiver::playerBonusChanged, *p->getBonusList().back(), true); } break; } } void ChangeObjPos::applyFirstCl(CClient *cl) { CGObjectInstance *obj = GS(cl)->getObjInstance(objid); if(flags & 1 && CGI->mh) CGI->mh->hideObject(obj); } void ChangeObjPos::applyCl(CClient *cl) { CGObjectInstance *obj = GS(cl)->getObjInstance(objid); if(flags & 1 && CGI->mh) CGI->mh->printObject(obj); cl->invalidatePaths(); } void PlayerEndsGame::applyCl(CClient *cl) { callAllInterfaces(cl, &IGameEventsReceiver::gameOver, player, victoryLossCheckResult); // In auto testing mode we always close client if red player won or lose if(!settings["session"]["testmap"].isNull() && player == PlayerColor(0)) handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not } void RemoveBonus::applyCl(CClient *cl) { cl->invalidatePaths(); switch(who) { case HERO: { const CGHeroInstance *h = GS(cl)->getHero(ObjectInstanceID(id)); callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, bonus, false); } break; case PLAYER: { //const PlayerState *p = GS(cl)->getPlayer(id); callInterfaceIfPresent(cl, PlayerColor(id), &IGameEventsReceiver::playerBonusChanged, bonus, false); } break; } } void RemoveObject::applyFirstCl(CClient *cl) { const CGObjectInstance *o = cl->getObj(id); if(CGI->mh) CGI->mh->hideObject(o, true); //notify interfaces about removal for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++) { //below line contains little cheat for AI so it will be aware of deletion of enemy heroes that moved or got re-covered by FoW //TODO: loose requirements as next AI related crashes appear, for example another player collects object that got re-covered by FoW, unsure if AI code workarounds this if(GS(cl)->isVisible(o, i->first) || (!cl->getPlayer(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first)) i->second->objectRemoved(o); } } void RemoveObject::applyCl(CClient *cl) { cl->invalidatePaths(); } void TryMoveHero::applyFirstCl(CClient *cl) { CGHeroInstance *h = GS(cl)->getHero(id); //check if playerint will have the knowledge about movement - if not, directly update maphandler for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++) { auto ps = GS(cl)->getPlayer(i->first); if(ps && (GS(cl)->isVisible(start - int3(1, 0, 0), i->first) || GS(cl)->isVisible(end - int3(1, 0, 0), i->first))) { if(ps->human) humanKnows = true; } } if(!CGI->mh) return; if(result == TELEPORTATION || result == EMBARK || result == DISEMBARK || !humanKnows) CGI->mh->hideObject(h, result == EMBARK && humanKnows); if(result == DISEMBARK) CGI->mh->printObject(h->boat); } void TryMoveHero::applyCl(CClient *cl) { const CGHeroInstance *h = cl->getHero(id); cl->invalidatePaths(); if(CGI->mh) { if(result == TELEPORTATION || result == EMBARK || result == DISEMBARK) CGI->mh->printObject(h, result == DISEMBARK); if(result == EMBARK) CGI->mh->hideObject(h->boat); } PlayerColor player = h->tempOwner; for(auto &i : cl->playerint) if(cl->getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES) i.second->tileRevealed(fowRevealed); //notify interfaces about move for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++) { if(GS(cl)->isVisible(start - int3(1, 0, 0), i->first) || GS(cl)->isVisible(end - int3(1, 0, 0), i->first)) { i->second->heroMoved(*this); } } //maphandler didn't get update from playerint, do it now //TODO: restructure nicely if(!humanKnows && CGI->mh) CGI->mh->printObject(h); } bool TryMoveHero::stopMovement() const { return result != SUCCESS && result != EMBARK && result != DISEMBARK && result != TELEPORTATION; } void NewStructures::applyCl(CClient *cl) { CGTownInstance *town = GS(cl)->getTown(tid); for(const auto & id : bid) { callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 1); } } void RazeStructures::applyCl (CClient *cl) { CGTownInstance *town = GS(cl)->getTown(tid); for(const auto & id : bid) { callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 2); } } void SetAvailableCreatures::applyCl(CClient *cl) { const CGDwelling *dw = static_cast(cl->getObj(tid)); //inform order about the change PlayerColor p; if(dw->ID == Obj::WAR_MACHINE_FACTORY) //War Machines Factory is not flaggable, it's "owned" by visitor p = cl->getTile(dw->visitablePos())->visitableObjects.back()->tempOwner; else p = dw->tempOwner; callInterfaceIfPresent(cl, p, &IGameEventsReceiver::availableCreaturesChanged, dw); } void SetHeroesInTown::applyCl(CClient *cl) { CGTownInstance *t = GS(cl)->getTown(tid); CGHeroInstance *hGarr = GS(cl)->getHero(this->garrison); CGHeroInstance *hVisit = GS(cl)->getHero(this->visiting); //inform all players that see this object for(auto i = cl->playerint.cbegin(); i != cl->playerint.cend(); ++i) { if(i->first >= PlayerColor::PLAYER_LIMIT) continue; if(GS(cl)->isVisible(t, i->first) || (hGarr && GS(cl)->isVisible(hGarr, i->first)) || (hVisit && GS(cl)->isVisible(hVisit, i->first))) { cl->playerint[i->first]->heroInGarrisonChange(t); } } } void HeroRecruited::applyCl(CClient *cl) { CGHeroInstance *h = GS(cl)->map->heroesOnMap.back(); if(h->subID != hid) { logNetwork->error("Something wrong with hero recruited!"); } bool needsPrinting = true; if(callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h)) { if(const CGTownInstance *t = GS(cl)->getTown(tid)) { callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroInGarrisonChange, t); needsPrinting = false; } } if(needsPrinting && CGI->mh) CGI->mh->printObject(h); } void GiveHero::applyCl(CClient *cl) { CGHeroInstance *h = GS(cl)->getHero(id); if(CGI->mh) CGI->mh->printObject(h); callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h); } void GiveHero::applyFirstCl(CClient *cl) { if(CGI->mh) CGI->mh->hideObject(GS(cl)->getHero(id)); } void InfoWindow::applyCl(CClient *cl) { std::string str; text.toString(str); if(!callInterfaceIfPresent(cl, player, &CGameInterface::showInfoDialog, str,components,(soundBase::soundID)soundID)) logNetwork->warn("We received InfoWindow for not our player..."); } void SetObjectProperty::applyCl(CClient *cl) { //inform all players that see this object for(auto it = cl->playerint.cbegin(); it != cl->playerint.cend(); ++it) { if(GS(cl)->isVisible(GS(cl)->getObjInstance(id), it->first)) callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::objectPropertyChanged, this); } } void HeroLevelUp::applyCl(CClient *cl) { const CGHeroInstance * hero = cl->getHero(heroId); assert(hero); callOnlyThatInterface(cl, player, &CGameInterface::heroGotLevel, hero, primskill, skills, queryID); } void CommanderLevelUp::applyCl(CClient *cl) { const CGHeroInstance * hero = cl->getHero(heroId); assert(hero); const CCommanderInstance * commander = hero->commander; assert(commander); assert(commander->armyObj); //is it possible for Commander to exist beyond armed instance? callOnlyThatInterface(cl, player, &CGameInterface::commanderGotLevel, commander, skills, queryID); } void BlockingDialog::applyCl(CClient *cl) { std::string str; text.toString(str); if(!callOnlyThatInterface(cl, player, &CGameInterface::showBlockingDialog, str, components, queryID, (soundBase::soundID)soundID, selection(), cancel())) logNetwork->warn("We received YesNoDialog for not our player..."); } void GarrisonDialog::applyCl(CClient *cl) { const CGHeroInstance *h = cl->getHero(hid); const CArmedInstance *obj = static_cast(cl->getObj(objid)); callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showGarrisonDialog, obj, h, removableUnits, queryID); } void ExchangeDialog::applyCl(CClient *cl) { callInterfaceIfPresent(cl, player, &IGameEventsReceiver::heroExchangeStarted, hero1, hero2, queryID); } void TeleportDialog::applyCl(CClient *cl) { callOnlyThatInterface(cl, player, &CGameInterface::showTeleportDialog, channel, exits, impassable, queryID); } void MapObjectSelectDialog::applyCl(CClient * cl) { callOnlyThatInterface(cl, player, &CGameInterface::showMapObjectSelectDialog, queryID, icon, title, description, objects); } void BattleStart::applyFirstCl(CClient *cl) { // Cannot use the usual code because curB is not set yet callOnlyThatBattleInterface(cl, info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject, info->tile, info->sides[0].hero, info->sides[1].hero); callOnlyThatBattleInterface(cl, info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject, info->tile, info->sides[0].hero, info->sides[1].hero); callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject, info->tile, info->sides[0].hero, info->sides[1].hero); callPrivilegedBattleInterfaces(cl, &IBattleEventsReceiver::battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject, info->tile, info->sides[0].hero, info->sides[1].hero); } void BattleStart::applyCl(CClient *cl) { cl->battleStarted(info); } void BattleNextRound::applyFirstCl(CClient *cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleNewRoundFirst, round); } void BattleNextRound::applyCl(CClient *cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleNewRound, round); } void BattleSetActiveStack::applyCl(CClient *cl) { if(!askPlayerInterface) return; const CStack *activated = GS(cl)->curB->battleGetStackByID(stack); PlayerColor playerToCall; //player that will move activated stack if (activated->hasBonusOfType(Bonus::HYPNOTIZED)) { playerToCall = (GS(cl)->curB->sides[0].color == activated->owner ? GS(cl)->curB->sides[1].color : GS(cl)->curB->sides[0].color); } else { playerToCall = activated->owner; } cl->startPlayerBattleAction(playerToCall); } void BattleTriggerEffect::applyCl(CClient * cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleTriggerEffect, *this); } void BattleUpdateGateState::applyFirstCl(CClient * cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleGateStateChanged, state); } void BattleResult::applyFirstCl(CClient *cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, this); cl->battleFinished(); } void BattleStackMoved::applyFirstCl(CClient *cl) { const CStack * movedStack = GS(cl)->curB->battleGetStackByID(stack); callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStackMoved, movedStack, tilesToMove, distance); } void BattleAttack::applyFirstCl(CClient *cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleAttack, this); } void BattleAttack::applyCl(CClient *cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, bsa, battleLog); } void StartAction::applyFirstCl(CClient *cl) { cl->curbaction = boost::make_optional(ba); callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionStarted, ba); } void BattleSpellCast::applyCl(CClient *cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleSpellCast, this); } void SetStackEffect::applyCl(CClient *cl) { //informing about effects callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksEffectsSet, *this); } void StacksInjured::applyCl(CClient *cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, stacks, battleLog); } void BattleResultsApplied::applyCl(CClient *cl) { callInterfaceIfPresent(cl, player1, &IGameEventsReceiver::battleResultsApplied); callInterfaceIfPresent(cl, player2, &IGameEventsReceiver::battleResultsApplied); callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleResultsApplied); } void BattleUnitsChanged::applyCl(CClient * cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleUnitsChanged, changedStacks, customEffects, battleLog); } void BattleObstaclesChanged::applyCl(CClient *cl) { //inform interfaces about removed obstacles callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleObstaclesChanged, changes); } void CatapultAttack::applyCl(CClient *cl) { //inform interfaces about catapult attack callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleCatapultAttacked, *this); } CGameState* CPackForClient::GS(CClient *cl) { return cl->gs; } void EndAction::applyCl(CClient *cl) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl->curbaction); cl->curbaction.reset(); } void PackageApplied::applyCl(CClient *cl) { callInterfaceIfPresent(cl, player, &IGameEventsReceiver::requestRealized, this); if(!CClient::waitingRequest.tryRemovingElement(requestID)) logNetwork->warn("Surprising server message! PackageApplied for unknown requestID!"); } void SystemMessage::applyCl(CClient *cl) { std::ostringstream str; str << "System message: " << text; logNetwork->error(str.str()); // usually used to receive error messages from server if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool()) LOCPLINT->cingconsole->print(str.str()); } void PlayerBlocked::applyCl(CClient *cl) { callInterfaceIfPresent(cl, player, &IGameEventsReceiver::playerBlocked, reason, startOrEnd == BLOCKADE_STARTED); } void YourTurn::applyCl(CClient *cl) { logNetwork->debug("Server gives turn to %s", player.getStr()); callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); callOnlyThatInterface(cl, player, &CGameInterface::yourTurn); } void SaveGameClient::applyCl(CClient *cl) { const auto stem = FileInfo::GetPathStem(fname); CResourceHandler::get("local")->createResource(stem.to_string() + ".vcgm1"); try { CSaveFile save(*CResourceHandler::get()->getResourceName(ResourceID(stem.to_string(), EResType::CLIENT_SAVEGAME))); cl->saveCommonState(save); save << *cl; } catch(std::exception &e) { logNetwork->error("Failed to save game:%s", e.what()); } } void PlayerMessageClient::applyCl(CClient *cl) { logNetwork->debug("Player %s sends a message: %s", player.getStr(), text); std::ostringstream str; if(player.isSpectator()) str << "Spectator: " << text; else str << cl->getPlayer(player)->nodeName() <<": " << text; if(LOCPLINT) LOCPLINT->cingconsole->print(str.str()); } void ShowInInfobox::applyCl(CClient *cl) { callInterfaceIfPresent(cl, player, &IGameEventsReceiver::showComp, c, text.toString()); } void AdvmapSpellCast::applyCl(CClient *cl) { cl->invalidatePaths(); auto caster = cl->getHero(casterID); if(caster) //consider notifying other interfaces that see hero? callInterfaceIfPresent(cl, caster->getOwner(), &IGameEventsReceiver::advmapSpellCast, caster, spellID); else logNetwork->error("Invalid hero instance"); } void ShowWorldViewEx::applyCl(CClient * cl) { callOnlyThatInterface(cl, player, &CGameInterface::showWorldViewEx, objectPositions); } void OpenWindow::applyCl(CClient *cl) { switch(window) { case RECRUITMENT_FIRST: case RECRUITMENT_ALL: { const CGDwelling *dw = dynamic_cast(cl->getObj(ObjectInstanceID(id1))); const CArmedInstance *dst = dynamic_cast(cl->getObj(ObjectInstanceID(id2))); callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, window == RECRUITMENT_FIRST ? 0 : -1); } break; case SHIPYARD_WINDOW: { const IShipyard *sy = IShipyard::castFrom(cl->getObj(ObjectInstanceID(id1))); callInterfaceIfPresent(cl, sy->o->tempOwner, &IGameEventsReceiver::showShipyardDialog, sy); } break; case THIEVES_GUILD: { //displays Thieves' Guild window (when hero enters Den of Thieves) const CGObjectInstance *obj = cl->getObj(ObjectInstanceID(id2)); callInterfaceIfPresent(cl, PlayerColor(id1), &IGameEventsReceiver::showThievesGuildWindow, obj); } break; case UNIVERSITY_WINDOW: { //displays University window (when hero enters University on adventure map) const IMarket *market = IMarket::castFrom(cl->getObj(ObjectInstanceID(id1))); const CGHeroInstance *hero = cl->getHero(ObjectInstanceID(id2)); callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero); } break; case MARKET_WINDOW: { //displays Thieves' Guild window (when hero enters Den of Thieves) const CGObjectInstance *obj = cl->getObj(ObjectInstanceID(id1)); const CGHeroInstance *hero = cl->getHero(ObjectInstanceID(id2)); const IMarket *market = IMarket::castFrom(obj); callInterfaceIfPresent(cl, cl->getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero); } break; case HILL_FORT_WINDOW: { //displays Hill fort window const CGObjectInstance *obj = cl->getObj(ObjectInstanceID(id1)); const CGHeroInstance *hero = cl->getHero(ObjectInstanceID(id2)); callInterfaceIfPresent(cl, cl->getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showHillFortWindow, obj, hero); } break; case PUZZLE_MAP: { callInterfaceIfPresent(cl, PlayerColor(id1), &IGameEventsReceiver::showPuzzleMap); } break; case TAVERN_WINDOW: const CGObjectInstance *obj1 = cl->getObj(ObjectInstanceID(id1)), *obj2 = cl->getObj(ObjectInstanceID(id2)); callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::showTavernWindow, obj2); break; } } void CenterView::applyCl(CClient *cl) { callInterfaceIfPresent(cl, player, &IGameEventsReceiver::centerView, pos, focusTime); } void NewObject::applyCl(CClient *cl) { cl->invalidatePaths(); const CGObjectInstance *obj = cl->getObj(id); if(CGI->mh) CGI->mh->printObject(obj, true); for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++) { if(GS(cl)->isVisible(obj, i->first)) i->second->newObject(obj); } } void SetAvailableArtifacts::applyCl(CClient *cl) { if(id < 0) //artifact merchants globally { callAllInterfaces(cl, &IGameEventsReceiver::availableArtifactsChanged, nullptr); } else { const CGBlackMarket *bm = dynamic_cast(cl->getObj(ObjectInstanceID(id))); assert(bm); callInterfaceIfPresent(cl, cl->getTile(bm->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::availableArtifactsChanged, bm); } } vcmi-0.99+git20190113.f06c8a87/client/NetPacksLobbyClient.cpp000066400000000000000000000066371342332007200230070ustar00rootroot00000000000000/* * NetPacksLobbyClient.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "lobby/CSelectionBase.h" #include "lobby/CLobbyScreen.h" #include "lobby/OptionsTab.h" #include "lobby/RandomMapTab.h" #include "lobby/SelectionTab.h" #include "lobby/CBonusSelection.h" #include "CServerHandler.h" #include "CGameInfo.h" #include "gui/CGuiHandler.h" #include "widgets/Buttons.h" #include "widgets/TextControls.h" #include "../lib/CConfigHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/NetPacksLobby.h" #include "../lib/serializer/Connection.h" bool LobbyClientConnected::applyOnLobbyHandler(CServerHandler * handler) { // Check if it's LobbyClientConnected for our client if(uuid == handler->c->uuid) { handler->c->connectionID = clientId; if(!settings["session"]["headless"].Bool()) GH.pushIntT(static_cast(handler->screenType)); handler->state = EClientState::LOBBY; return true; } return false; } void LobbyClientConnected::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) { } bool LobbyClientDisconnected::applyOnLobbyHandler(CServerHandler * handler) { if(clientId != c->connectionID) return false; handler->stopServerConnection(); return true; } void LobbyClientDisconnected::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) { GH.popInts(1); } void LobbyChatMessage::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) { if(lobby) { lobby->card->chat->addNewMessage(playerName + ": " + message); lobby->card->setChat(true); if(lobby->buttonChat) lobby->buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL); } } void LobbyGuiAction::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) { if(!handler->isGuest()) return; switch(action) { case NO_TAB: lobby->toggleTab(lobby->curTab); break; case OPEN_OPTIONS: lobby->toggleTab(lobby->tabOpt); break; case OPEN_SCENARIO_LIST: lobby->toggleTab(lobby->tabSel); break; case OPEN_RANDOM_MAP_OPTIONS: lobby->toggleTab(lobby->tabRand); break; } } bool LobbyStartGame::applyOnLobbyHandler(CServerHandler * handler) { if(handler->state == EClientState::GAMEPLAY) { handler->endGameplay(false, true); } handler->state = EClientState::STARTING; if(handler->si->mode != StartInfo::LOAD_GAME) { handler->si = initializedStartInfo; } if(settings["session"]["headless"].Bool()) handler->startGameplay(); return true; } void LobbyStartGame::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) { GH.pushIntT(std::bind(&CServerHandler::startGameplay, handler)); } bool LobbyUpdateState::applyOnLobbyHandler(CServerHandler * handler) { hostChanged = state.hostClientId != handler->hostClientId; static_cast(*handler) = state; return true; } void LobbyUpdateState::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) { if(!lobby->bonusSel && handler->si->campState && handler->state == EClientState::LOBBY_CAMPAIGN) { lobby->bonusSel = std::make_shared(); GH.pushInt(lobby->bonusSel); } if(lobby->bonusSel) lobby->bonusSel->updateAfterStateChange(); else lobby->updateAfterStateChange(); if(hostChanged) lobby->toggleMode(handler->isHost()); } vcmi-0.99+git20190113.f06c8a87/client/SDLMain.h000066400000000000000000000005661342332007200200370ustar00rootroot00000000000000/* SDLMain.m - main entry point for our Cocoa-ized SDL app Initial Version: Darrell Walisser Non-NIB-Code & other changes: Max Horn Feel free to customize this file to suit your needs */ #ifndef _SDLMain_h_ #define _SDLMain_h_ #import @interface SDLMain : NSObject @end #endif /* _SDLMain_h_ */ vcmi-0.99+git20190113.f06c8a87/client/SDLMain.m000066400000000000000000000260001342332007200200330ustar00rootroot00000000000000/* SDLMain.m - main entry point for our Cocoa-ized SDL app Initial Version: Darrell Walisser Non-NIB-Code & other changes: Max Horn Feel free to customize this file to suit your needs */ #include "SDL.h" #include "SDLMain.h" #include /* for MAXPATHLEN */ #include /* For some reaon, Apple removed setAppleMenu from the headers in 10.4, but the method still is there and works. To avoid warnings, we declare it ourselves here. */ @interface NSApplication(SDL_Missing_Methods) - (void)setAppleMenu:(NSMenu *)menu; @end /* Use this flag to determine whether we use SDLMain.nib or not */ #define SDL_USE_NIB_FILE 0 /* Use this flag to determine whether we use CPS (docking) or not */ #define SDL_USE_CPS 1 #ifdef SDL_USE_CPS /* Portions of CPS.h */ typedef struct CPSProcessSerNum { UInt32 lo; UInt32 hi; } CPSProcessSerNum; extern OSErr CPSGetCurrentProcess( CPSProcessSerNum *psn); extern OSErr CPSEnableForegroundOperation( CPSProcessSerNum *psn, UInt32 _arg2, UInt32 _arg3, UInt32 _arg4, UInt32 _arg5); extern OSErr CPSSetFrontProcess( CPSProcessSerNum *psn); #endif /* SDL_USE_CPS */ static int gArgc; static char **gArgv; static BOOL gFinderLaunch; static BOOL gCalledAppMainline = FALSE; static NSString *getApplicationName(void) { const NSDictionary *dict; NSString *appName = 0; /* Determine the application name */ dict = (const NSDictionary *)CFBundleGetInfoDictionary(CFBundleGetMainBundle()); if (dict) appName = [dict objectForKey: @"CFBundleName"]; if (![appName length]) appName = [[NSProcessInfo processInfo] processName]; return appName; } #if SDL_USE_NIB_FILE /* A helper category for NSString */ @interface NSString (ReplaceSubString) - (NSString *)stringByReplacingRange:(NSRange)aRange with:(NSString *)aString; @end #endif @interface SDLApplication : NSApplication @end @implementation SDLApplication /* Invoked from the Quit menu item */ - (void)terminate:(id)sender { /* Post a SDL_QUIT event */ SDL_Event event; event.type = SDL_QUIT; SDL_PushEvent(&event); } @end /* The main class of the application, the application's delegate */ @implementation SDLMain /* Set the working directory to the .app's parent directory */ - (void) setupWorkingDirectory:(BOOL)shouldChdir { if (shouldChdir) { char parentdir[MAXPATHLEN]; CFURLRef url = CFBundleCopyBundleURL(CFBundleGetMainBundle()); CFURLRef url2 = CFURLCreateCopyDeletingLastPathComponent(0, url); if (CFURLGetFileSystemRepresentation(url2, 1, (UInt8 *)parentdir, MAXPATHLEN)) { chdir(parentdir); /* chdir to the binary app's parent */ } CFRelease(url); CFRelease(url2); } } #if SDL_USE_NIB_FILE /* Fix menu to contain the real app name instead of "SDL App" */ - (void)fixMenu:(NSMenu *)aMenu withAppName:(NSString *)appName { NSRange aRange; NSEnumerator *enumerator; NSMenuItem *menuItem; aRange = [[aMenu title] rangeOfString:@"SDL App"]; if (aRange.length != 0) [aMenu setTitle: [[aMenu title] stringByReplacingRange:aRange with:appName]]; enumerator = [[aMenu itemArray] objectEnumerator]; while ((menuItem = [enumerator nextObject])) { aRange = [[menuItem title] rangeOfString:@"SDL App"]; if (aRange.length != 0) [menuItem setTitle: [[menuItem title] stringByReplacingRange:aRange with:appName]]; if ([menuItem hasSubmenu]) [self fixMenu:[menuItem submenu] withAppName:appName]; } [ aMenu sizeToFit ]; } #else static void setApplicationMenu(void) { /* warning: this code is very odd */ NSMenu *appleMenu; NSMenuItem *menuItem; NSString *title; NSString *appName; appName = getApplicationName(); appleMenu = [[NSMenu alloc] initWithTitle:@""]; /* Add menu items */ title = [@"About " stringByAppendingString:appName]; [appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; [appleMenu addItem:[NSMenuItem separatorItem]]; title = [@"Hide " stringByAppendingString:appName]; [appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"]; menuItem = (NSMenuItem *)[appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; [menuItem setKeyEquivalentModifierMask:(NSAlternateKeyMask|NSCommandKeyMask)]; [appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; [appleMenu addItem:[NSMenuItem separatorItem]]; title = [@"Quit " stringByAppendingString:appName]; [appleMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"]; /* Put menu into the menubar */ menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; [menuItem setSubmenu:appleMenu]; [[NSApp mainMenu] addItem:menuItem]; /* Tell the application object that this is now the application menu */ [NSApp setAppleMenu:appleMenu]; /* Finally give up our references to the objects */ [appleMenu release]; [menuItem release]; } /* Create a window menu */ static void setupWindowMenu(void) { NSMenu *windowMenu; NSMenuItem *windowMenuItem; NSMenuItem *menuItem; windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; /* "Minimize" item */ menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; [windowMenu addItem:menuItem]; [menuItem release]; /* Put menu into the menubar */ windowMenuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; [windowMenuItem setSubmenu:windowMenu]; [[NSApp mainMenu] addItem:windowMenuItem]; /* Tell the application object that this is now the window menu */ [NSApp setWindowsMenu:windowMenu]; /* Finally give up our references to the objects */ [windowMenu release]; [windowMenuItem release]; } /* Replacement for NSApplicationMain */ static void CustomApplicationMain (int argc, char **argv) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; SDLMain *sdlMain; /* Ensure the application object is initialised */ [SDLApplication sharedApplication]; #ifdef SDL_USE_CPS { CPSProcessSerNum PSN; /* Tell the dock about us */ if (!CPSGetCurrentProcess(&PSN)) if (!CPSEnableForegroundOperation(&PSN,0x03,0x3C,0x2C,0x1103)) if (!CPSSetFrontProcess(&PSN)) [SDLApplication sharedApplication]; } #endif /* SDL_USE_CPS */ /* Set up the menubar */ [NSApp setMainMenu:[[NSMenu alloc] init]]; setApplicationMenu(); setupWindowMenu(); /* Create SDLMain and make it the app delegate */ sdlMain = [[SDLMain alloc] init]; [NSApp setDelegate:sdlMain]; /* Start the main event loop */ [NSApp run]; [sdlMain release]; [pool release]; } #endif /* * Catch document open requests...this lets us notice files when the app * was launched by double-clicking a document, or when a document was * dragged/dropped on the app's icon. You need to have a * CFBundleDocumentsType section in your Info.plist to get this message, * apparently. * * Files are added to gArgv, so to the app, they'll look like command line * arguments. Previously, apps launched from the finder had nothing but * an argv[0]. * * This message may be received multiple times to open several docs on launch. * * This message is ignored once the app's mainline has been called. */ - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { const char *temparg; size_t arglen; char *arg; char **newargv; if (!gFinderLaunch) /* MacOS is passing command line args. */ return FALSE; if (gCalledAppMainline) /* app has started, ignore this document. */ return FALSE; temparg = [filename UTF8String]; arglen = SDL_strlen(temparg) + 1; arg = (char *) SDL_malloc(arglen); if (arg == NULL) return FALSE; newargv = (char **) realloc(gArgv, sizeof (char *) * (gArgc + 2)); if (newargv == NULL) { SDL_free(arg); return FALSE; } gArgv = newargv; SDL_strlcpy(arg, temparg, arglen); gArgv[gArgc++] = arg; gArgv[gArgc] = NULL; return TRUE; } /* Called when the internal event loop has just started running */ - (void) applicationDidFinishLaunching: (NSNotification *) note { int status; /* Set the working directory to the .app's parent directory */ [self setupWorkingDirectory:gFinderLaunch]; #if SDL_USE_NIB_FILE /* Set the main menu to contain the real app name instead of "SDL App" */ [self fixMenu:[NSApp mainMenu] withAppName:getApplicationName()]; #endif /* Hand off to main application code */ gCalledAppMainline = TRUE; status = SDL_main (gArgc, gArgv); /* We're done, thank you for playing */ exit(status); } @end @implementation NSString (ReplaceSubString) - (NSString *)stringByReplacingRange:(NSRange)aRange with:(NSString *)aString { unsigned int bufferSize; unsigned int selfLen = [self length]; unsigned int aStringLen = [aString length]; unichar *buffer; NSRange localRange; NSString *result; bufferSize = selfLen + aStringLen - aRange.length; buffer = (unichar *)NSAllocateMemoryPages(bufferSize*sizeof(unichar)); /* Get first part into buffer */ localRange.location = 0; localRange.length = aRange.location; [self getCharacters:buffer range:localRange]; /* Get middle part into buffer */ localRange.location = 0; localRange.length = aStringLen; [aString getCharacters:(buffer+aRange.location) range:localRange]; /* Get last part into buffer */ localRange.location = aRange.location + aRange.length; localRange.length = selfLen - localRange.location; [self getCharacters:(buffer+aRange.location+aStringLen) range:localRange]; /* Build output string */ result = [NSString stringWithCharacters:buffer length:bufferSize]; NSDeallocateMemoryPages(buffer, bufferSize); return result; } @end #ifdef main # undef main #endif /* Main entry point to executable - should *not* be SDL_main! */ int main (int argc, char **argv) { /* Copy the arguments into a global variable */ /* This is passed if we are launched by double-clicking */ if ( argc >= 2 && strncmp (argv[1], "-psn", 4) == 0 ) { gArgv = (char **) SDL_malloc(sizeof (char *) * 2); gArgv[0] = argv[0]; gArgv[1] = NULL; gArgc = 1; gFinderLaunch = YES; } else { int i; gArgc = argc; gArgv = (char **) SDL_malloc(sizeof (char *) * (argc+1)); for (i = 0; i <= argc; i++) gArgv[i] = argv[i]; gFinderLaunch = NO; } #if SDL_USE_NIB_FILE [SDLApplication poseAsClass:[NSApplication class]]; NSApplicationMain (argc, argv); #else CustomApplicationMain (argc, argv); #endif return 0; } vcmi-0.99+git20190113.f06c8a87/client/SDLRWwrapper.cpp000066400000000000000000000040011342332007200214230ustar00rootroot00000000000000/* * SDLRWwrapper.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "SDLRWwrapper.h" #include "../lib/filesystem/CInputStream.h" #include static inline CInputStream* get_stream(SDL_RWops* context) { return static_cast(context->hidden.unknown.data1); } static Sint64 impl_size(SDL_RWops* context) { return get_stream(context)->getSize(); } static Sint64 impl_seek(SDL_RWops* context, Sint64 offset, int whence) { auto stream = get_stream(context); switch (whence) { case RW_SEEK_SET: return stream->seek(offset); break; case RW_SEEK_CUR: return stream->seek(stream->tell() + offset); break; case RW_SEEK_END: return stream->seek(stream->getSize() + offset); break; default: return -1; } } static std::size_t impl_read(SDL_RWops* context, void *ptr, size_t size, size_t maxnum) { auto stream = get_stream(context); auto oldpos = stream->tell(); auto count = stream->read(static_cast(ptr), size*maxnum); if (count != 0 && count != size*maxnum) { // if not a whole amount of objects of size has been read, we need to seek stream->seek(oldpos + size * (count / size)); } return count / size; } static std::size_t impl_write(SDL_RWops* context, const void *ptr, size_t size, size_t num) { // writing is not supported return 0; } static int impl_close(SDL_RWops* context) { if (context == nullptr) return 0; delete get_stream(context); SDL_FreeRW(context); return 0; } SDL_RWops* MakeSDLRWops(std::unique_ptr in) { SDL_RWops* result = SDL_AllocRW(); if (!result) return nullptr; result->size = &impl_size; result->seek = &impl_seek; result->read = &impl_read; result->write = &impl_write; result->close = &impl_close; result->type = SDL_RWOPS_UNKNOWN; result->hidden.unknown.data1 = in.release(); return result; } vcmi-0.99+git20190113.f06c8a87/client/SDLRWwrapper.h000066400000000000000000000005241342332007200210760ustar00rootroot00000000000000/* * SDLRWwrapper.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once struct SDL_RWops; class CInputStream; SDL_RWops* MakeSDLRWops(std::unique_ptr in); vcmi-0.99+git20190113.f06c8a87/client/StdInc.cpp000066400000000000000000000000661342332007200203220ustar00rootroot00000000000000// Creates the precompiled header #include "StdInc.h"vcmi-0.99+git20190113.f06c8a87/client/StdInc.h000066400000000000000000000004121342332007200177620ustar00rootroot00000000000000#pragma once #include "../Global.h" #include "gui/SDL_Compat.h" // This header should be treated as a pre compiled header file(PCH) in the compiler building settings. // Here you can add specific libraries and macros which are specific to this project. vcmi-0.99+git20190113.f06c8a87/client/VCMI_client.cbp000066400000000000000000000222611342332007200212150ustar00rootroot00000000000000 vcmi-0.99+git20190113.f06c8a87/client/VCMI_client.rc000066400000000000000000000000341342332007200210470ustar00rootroot00000000000000IDI_ICON1 ICON "vcmi.ico"vcmi-0.99+git20190113.f06c8a87/client/VCMI_client.vcxproj000066400000000000000000000413661342332007200221530ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6} VCMI_client 10.0.17763.0 Application Unicode true v141 Application Unicode true v140_xp Application Unicode v140_xp Application Unicode v140_xp <_ProjectFileVersion>10.0.30128.1 .. $(VCMI_Out) $(Configuration)\ $(Configuration)\ $(VCMI_Out) $(VCMI_Out) $(Configuration)\ $(Configuration)\ AllRules.ruleset AllRules.ruleset AllRules.ruleset AllRules.ruleset 4251;%(DisableSpecificWarnings) NoListing Use StdInc.h /MP4 /Zm150 $(FFMPEGDIR);$(SDLDIR);$(BOOSTDIR);$(AdditionalIncludeDirectories) avcodec.lib;avdevice.lib;avfilter.lib;avformat.lib;avutil.lib;postproc.lib;swresample.lib;swscale.lib;SDL.lib;zlib.lib;SDL_image.lib;SDL_ttf.lib;SDL_mixer.lib;VCMI_lib.lib;%(AdditionalDependencies) NotSet true true $(FFMPEGDIR)\lib;.;..\..\libs;.. $(ProjectDir)DPIaware.manifest;%(AdditionalManifestFiles) false false 4251;%(DisableSpecificWarnings) NoListing Use StdInc.h /MP4 /Zm150 SDL.lib;zlib.lib;SDL_image.lib;SDL_ttf.lib;SDL_mixer.lib;VCMI_lib.lib;%(AdditionalDependencies) LinkVerbose false true $(ProjectDir)DPIaware.manifest;%(AdditionalManifestFiles) Use StdInc.h $(FFMPEGDIR);$(SDLDIR);$(BOOSTDIR);$(AdditionalIncludeDirectories) true avcodec.lib;avdevice.lib;avfilter.lib;avformat.lib;avutil.lib;postproc.lib;swresample.lib;swscale.lib;zlib.lib;SDL2.lib;SDL2main.lib;VCMI_lib.lib;SDL2_mixer.lib;SDL2_image.lib;SDL2_ttf.lib;%(AdditionalDependencies) NotSet NotSet $(VCMI_Out) false true MultiplyDefinedSymbolOnly /LTCG /d2:-notypeopt %(AdditionalOptions) /MP4 /Zm150 Use StdInc.h SDL.lib;zlib.lib;SDL_image.lib;SDL_ttf.lib;SDL_mixer.lib;VCMI_lib.lib;%(AdditionalDependencies) NotSet NotSet Create {b952ffc5-3039-4de1-9f08-90acda483d8f} vcmi-0.99+git20190113.f06c8a87/client/VCMI_client.vcxproj.filters000066400000000000000000000242711342332007200236160ustar00rootroot00000000000000 windows windows windows windows windows windows windows windows windows windows windows widgets widgets widgets widgets widgets widgets widgets widgets widgets battle battle battle battle gui gui gui gui gui gui gui windows windows {c25dc848-6e1b-4a9b-b6f2-517626e9a23d} {cec5376d-0f32-475e-bf51-3dbae35c6b98} {2b5b57d6-28ba-4bcf-9691-0977171866d9} {c9fcce39-f3af-4621-996c-0df7695134bd} windows windows windows windows windows windows windows windows windows windows windows widgets widgets widgets widgets widgets widgets widgets widgets widgets battle battle battle battle gui gui gui gui gui gui gui gui gui windows windows vcmi-0.99+git20190113.f06c8a87/client/battle/000077500000000000000000000000001342332007200177035ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/client/battle/CBattleAnimations.cpp000066400000000000000000001017251342332007200237560ustar00rootroot00000000000000/* * CBattleAnimations.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CBattleAnimations.h" #include #include "CBattleInterfaceClasses.h" #include "CBattleInterface.h" #include "CCreatureAnimation.h" #include "../CGameInfo.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../Graphics.h" #include "../gui/CAnimation.h" #include "../gui/CCursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/SDL_Extensions.h" #include "../../CCallback.h" #include "../../lib/CStack.h" #include "../../lib/CTownHandler.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/spells/CSpellHandler.h" CBattleAnimation::CBattleAnimation(CBattleInterface * _owner) : owner(_owner), ID(_owner->animIDhelper++) { logAnim->trace("Animation #%d created", ID); } CBattleAnimation::~CBattleAnimation() { logAnim->trace("Animation #%d deleted", ID); } void CBattleAnimation::endAnim() { logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name()); for(auto & elem : owner->pendingAnims) { if(elem.first == this) { elem.first = nullptr; } } } bool CBattleAnimation::isEarliest(bool perStackConcurrency) { int lowestMoveID = owner->animIDhelper + 5; CBattleStackAnimation * thAnim = dynamic_cast(this); CEffectAnimation * thSen = dynamic_cast(this); for(auto & elem : owner->pendingAnims) { CBattleStackAnimation * stAnim = dynamic_cast(elem.first); CEffectAnimation * sen = dynamic_cast(elem.first); if(perStackConcurrency && stAnim && thAnim && stAnim->stack->ID != thAnim->stack->ID) continue; if(perStackConcurrency && sen && thSen && sen != thSen) continue; CReverseAnimation * revAnim = dynamic_cast(stAnim); if(revAnim && thAnim && stAnim && stAnim->stack->ID == thAnim->stack->ID && revAnim->priority) return false; if(elem.first) vstd::amin(lowestMoveID, elem.first->ID); } return (ID == lowestMoveID) || (lowestMoveID == (owner->animIDhelper + 5)); } CBattleStackAnimation::CBattleStackAnimation(CBattleInterface * owner, const CStack * stack) : CBattleAnimation(owner), myAnim(owner->creAnims[stack->ID]), stack(stack) { assert(myAnim); } void CAttackAnimation::nextFrame() { if(myAnim->getType() != group) { myAnim->setType(group); myAnim->onAnimationReset += std::bind(&CAttackAnimation::endAnim, this); } if(!soundPlayed) { if(shooting) CCS->soundh->playSound(battle_sound(attackingStack->getCreature(), shoot)); else CCS->soundh->playSound(battle_sound(attackingStack->getCreature(), attack)); soundPlayed = true; } CBattleAnimation::nextFrame(); } void CAttackAnimation::endAnim() { myAnim->setType(CCreatureAnim::HOLDING); CBattleStackAnimation::endAnim(); } bool CAttackAnimation::checkInitialConditions() { for(auto & elem : owner->pendingAnims) { CBattleStackAnimation * stAnim = dynamic_cast(elem.first); CReverseAnimation * revAnim = dynamic_cast(stAnim); if(revAnim && attackedStack) // enemy must be fully reversed { if (revAnim->stack->ID == attackedStack->ID) return false; } } return isEarliest(false); } CAttackAnimation::CAttackAnimation(CBattleInterface *_owner, const CStack *attacker, BattleHex _dest, const CStack *defender) : CBattleStackAnimation(_owner, attacker), shooting(false), group(CCreatureAnim::SHOOT_FRONT), soundPlayed(false), dest(_dest), attackedStack(defender), attackingStack(attacker) { assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n"); attackingStackPosBeforeReturn = attackingStack->getPosition(); } CDefenceAnimation::CDefenceAnimation(StackAttackedInfo _attackedInfo, CBattleInterface * _owner) : CBattleStackAnimation(_owner, _attackedInfo.defender), attacker(_attackedInfo.attacker), rangedAttack(_attackedInfo.indirectAttack), killed(_attackedInfo.killed), timeToWait(0) { logAnim->debug("Created defence anim for %s", _attackedInfo.defender->getName()); } bool CDefenceAnimation::init() { if(attacker == nullptr && owner->battleEffects.size() > 0) return false; ui32 lowestMoveID = owner->animIDhelper + 5; for(auto & elem : owner->pendingAnims) { CDefenceAnimation * defAnim = dynamic_cast(elem.first); if(defAnim && defAnim->stack->ID != stack->ID) continue; CAttackAnimation * attAnim = dynamic_cast(elem.first); if(attAnim && attAnim->stack->ID != stack->ID) continue; CEffectAnimation * sen = dynamic_cast(elem.first); if (sen) continue; CReverseAnimation * animAsRev = dynamic_cast(elem.first); if(animAsRev) return false; if(elem.first) vstd::amin(lowestMoveID, elem.first->ID); } if(ID > lowestMoveID) return false; //reverse unit if necessary if(attacker && owner->getCurrentPlayerInterface()->cb->isToReverse(stack->getPosition(), attacker->getPosition(), owner->creDir[stack->ID], attacker->doubleWide(), owner->creDir[attacker->ID])) { owner->addNewAnim(new CReverseAnimation(owner, stack, stack->getPosition(), true)); return false; } //unit reversed if(rangedAttack) //delay hit animation { for(std::list::const_iterator it = owner->projectiles.begin(); it != owner->projectiles.end(); ++it) { if(it->creID == attacker->getCreature()->idNumber) { return false; } } } // synchronize animation with attacker, unless defending or attacked by shooter: // wait for 1/2 of attack animation if (!rangedAttack && getMyAnimType() != CCreatureAnim::DEFENCE) { float frameLength = AnimationControls::getCreatureAnimationSpeed( stack->getCreature(), owner->creAnims[stack->ID].get(), getMyAnimType()); timeToWait = myAnim->framesInGroup(getMyAnimType()) * frameLength / 2; myAnim->setType(CCreatureAnim::HOLDING); } else { timeToWait = 0; startAnimation(); } return true; //initialized successfuly } std::string CDefenceAnimation::getMySound() { if(killed) return battle_sound(stack->getCreature(), killed); else if(stack->defendingAnim) return battle_sound(stack->getCreature(), defend); else return battle_sound(stack->getCreature(), wince); } CCreatureAnim::EAnimType CDefenceAnimation::getMyAnimType() { if(killed) { if(rangedAttack && myAnim->framesInGroup(CCreatureAnim::DEATH_RANGED) > 0) return CCreatureAnim::DEATH_RANGED; else return CCreatureAnim::DEATH; } if(stack->defendingAnim) return CCreatureAnim::DEFENCE; else return CCreatureAnim::HITTED; } void CDefenceAnimation::startAnimation() { CCS->soundh->playSound(getMySound()); myAnim->setType(getMyAnimType()); myAnim->onAnimationReset += std::bind(&CDefenceAnimation::endAnim, this); } void CDefenceAnimation::nextFrame() { if (timeToWait > 0) { timeToWait -= float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000; if (timeToWait <= 0) startAnimation(); } CBattleAnimation::nextFrame(); } void CDefenceAnimation::endAnim() { if(killed) { if(rangedAttack && myAnim->framesInGroup(CCreatureAnim::DEAD_RANGED) > 0) myAnim->setType(CCreatureAnim::DEAD_RANGED); else myAnim->setType(CCreatureAnim::DEAD); } else { myAnim->setType(CCreatureAnim::HOLDING); } CBattleAnimation::endAnim(); delete this; } CDummyAnimation::CDummyAnimation(CBattleInterface * _owner, int howManyFrames) : CBattleAnimation(_owner), counter(0), howMany(howManyFrames) { logAnim->debug("Created dummy animation for %d frames", howManyFrames); } bool CDummyAnimation::init() { return true; } void CDummyAnimation::nextFrame() { counter++; if(counter > howMany) endAnim(); } void CDummyAnimation::endAnim() { CBattleAnimation::endAnim(); delete this; } bool CMeleeAttackAnimation::init() { if(!CAttackAnimation::checkInitialConditions()) return false; if(!attackingStack || myAnim->isDead()) { endAnim(); return false; } bool toReverse = owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStackPosBeforeReturn, attackedStack->getPosition(), owner->creDir[stack->ID], attackedStack->doubleWide(), owner->creDir[attackedStack->ID]); if(toReverse) { owner->addNewAnim(new CReverseAnimation(owner, stack, attackingStackPosBeforeReturn, true)); return false; } // opponent must face attacker ( = different directions) before he can be attacked if(attackingStack && attackedStack && owner->creDir[attackingStack->ID] == owner->creDir[attackedStack->ID]) return false; //reversed shooting = false; static const CCreatureAnim::EAnimType mutPosToGroup[] = { CCreatureAnim::ATTACK_UP, CCreatureAnim::ATTACK_UP, CCreatureAnim::ATTACK_FRONT, CCreatureAnim::ATTACK_DOWN, CCreatureAnim::ATTACK_DOWN, CCreatureAnim::ATTACK_FRONT }; static const CCreatureAnim::EAnimType mutPosToGroup2H[] = { CCreatureAnim::VCMI_2HEX_UP, CCreatureAnim::VCMI_2HEX_UP, CCreatureAnim::VCMI_2HEX_FRONT, CCreatureAnim::VCMI_2HEX_DOWN, CCreatureAnim::VCMI_2HEX_DOWN, CCreatureAnim::VCMI_2HEX_FRONT }; int revShiftattacker = (attackingStack->side == BattleSide::ATTACKER ? -1 : 1); int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest); if(mutPos == -1 && attackingStack->doubleWide()) { mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, attackedStack->getPosition()); } if (mutPos == -1 && attackedStack->doubleWide()) { mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, attackedStack->occupiedHex()); } if (mutPos == -1 && attackedStack->doubleWide() && attackingStack->doubleWide()) { mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, attackedStack->occupiedHex()); } switch(mutPos) //attack direction { case 0: case 1: case 2: case 3: case 4: case 5: group = mutPosToGroup[mutPos]; if(attackingStack->hasBonusOfType(Bonus::TWO_HEX_ATTACK_BREATH)) { CCreatureAnim::EAnimType group2H = mutPosToGroup2H[mutPos]; if(myAnim->framesInGroup(group2H)>0) group = group2H; } break; default: logGlobal->error("Critical Error! Wrong dest in stackAttacking! dest: %d; attacking stack pos: %d; mutual pos: %d", dest.hex, attackingStackPosBeforeReturn, mutPos); group = CCreatureAnim::ATTACK_FRONT; break; } return true; } CMeleeAttackAnimation::CMeleeAttackAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked) : CAttackAnimation(_owner, attacker, _dest, _attacked) { logAnim->debug("Created melee attack anim for %s", attacker->getName()); } void CMeleeAttackAnimation::endAnim() { CAttackAnimation::endAnim(); delete this; } bool CMovementAnimation::shouldRotate() { Point begPosition = CClickableHex::getXYUnitAnim(oldPos, stack, owner); Point endPosition = CClickableHex::getXYUnitAnim(nextHex, stack, owner); if((begPosition.x > endPosition.x) && owner->creDir[stack->ID] == true) { return true; } else if ((begPosition.x < endPosition.x) && owner->creDir[stack->ID] == false) { return true; } return false; } bool CMovementAnimation::init() { if( !isEarliest(false) ) return false; if(!stack || myAnim->isDead()) { endAnim(); return false; } if(owner->creAnims[stack->ID]->framesInGroup(CCreatureAnim::MOVING) == 0 || stack->hasBonus(Selector::typeSubtype(Bonus::FLYING, 1))) { //no movement or teleport, end immediately endAnim(); return false; } //reverse unit if necessary if(shouldRotate()) { // it seems that H3 does NOT plays full rotation animation here in most situations // Logical since it takes quite a lot of time if (curentMoveIndex == 0) // full rotation only for moving towards first tile. { owner->addNewAnim(new CReverseAnimation(owner, stack, oldPos, true)); return false; } else { CReverseAnimation::rotateStack(owner, stack, oldPos); } } if(myAnim->getType() != CCreatureAnim::MOVING) { myAnim->setType(CCreatureAnim::MOVING); } if (owner->moveSoundHander == -1) { owner->moveSoundHander = CCS->soundh->playSound(battle_sound(stack->getCreature(), move), -1); } Point begPosition = CClickableHex::getXYUnitAnim(oldPos, stack, owner); Point endPosition = CClickableHex::getXYUnitAnim(nextHex, stack, owner); timeToMove = AnimationControls::getMovementDuration(stack->getCreature()); begX = begPosition.x; begY = begPosition.y; progress = 0; distanceX = endPosition.x - begPosition.x; distanceY = endPosition.y - begPosition.y; if (stack->hasBonus(Selector::type(Bonus::FLYING))) { float distance = sqrt(distanceX * distanceX + distanceY * distanceY); timeToMove *= AnimationControls::getFlightDistance(stack->getCreature()) / distance; } return true; } void CMovementAnimation::nextFrame() { progress += float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000 * timeToMove; //moving instructions myAnim->pos.x = static_cast(begX + distanceX * progress ); myAnim->pos.y = static_cast(begY + distanceY * progress ); CBattleAnimation::nextFrame(); if(progress >= 1.0) { // Sets the position of the creature animation sprites Point coords = CClickableHex::getXYUnitAnim(nextHex, stack, owner); myAnim->pos = coords; // true if creature haven't reached the final destination hex if ((curentMoveIndex + 1) < destTiles.size()) { // update the next hex field which has to be reached by the stack curentMoveIndex++; oldPos = nextHex; nextHex = destTiles[curentMoveIndex]; // re-init animation for(auto & elem : owner->pendingAnims) { if (elem.first == this) { elem.second = false; break; } } } else endAnim(); } } void CMovementAnimation::endAnim() { assert(stack); myAnim->pos = CClickableHex::getXYUnitAnim(nextHex, stack, owner); CBattleAnimation::endAnim(); owner->addNewAnim(new CMovementEndAnimation(owner, stack, nextHex)); if(owner->moveSoundHander != -1) { CCS->soundh->stopSound(owner->moveSoundHander); owner->moveSoundHander = -1; } delete this; } CMovementAnimation::CMovementAnimation(CBattleInterface *_owner, const CStack *_stack, std::vector _destTiles, int _distance) : CBattleStackAnimation(_owner, _stack), destTiles(_destTiles), curentMoveIndex(0), oldPos(stack->getPosition()), begX(0), begY(0), distanceX(0), distanceY(0), timeToMove(0.0), progress(0.0), nextHex(destTiles.front()) { logAnim->debug("Created movement anim for %s", stack->getName()); } CMovementEndAnimation::CMovementEndAnimation(CBattleInterface * _owner, const CStack * _stack, BattleHex destTile) : CBattleStackAnimation(_owner, _stack), destinationTile(destTile) { logAnim->debug("Created movement end anim for %s", stack->getName()); } bool CMovementEndAnimation::init() { if( !isEarliest(true) ) return false; if(!stack || myAnim->framesInGroup(CCreatureAnim::MOVE_END) == 0 || myAnim->isDead()) { endAnim(); return false; } CCS->soundh->playSound(battle_sound(stack->getCreature(), endMoving)); myAnim->setType(CCreatureAnim::MOVE_END); myAnim->onAnimationReset += std::bind(&CMovementEndAnimation::endAnim, this); return true; } void CMovementEndAnimation::endAnim() { CBattleAnimation::endAnim(); if(myAnim->getType() != CCreatureAnim::DEAD) myAnim->setType(CCreatureAnim::HOLDING); //resetting to default CCS->curh->show(); delete this; } CMovementStartAnimation::CMovementStartAnimation(CBattleInterface * _owner, const CStack * _stack) : CBattleStackAnimation(_owner, _stack) { logAnim->debug("Created movement start anim for %s", stack->getName()); } bool CMovementStartAnimation::init() { if( !isEarliest(false) ) return false; if(!stack || myAnim->isDead()) { CMovementStartAnimation::endAnim(); return false; } CCS->soundh->playSound(battle_sound(stack->getCreature(), startMoving)); myAnim->setType(CCreatureAnim::MOVE_START); myAnim->onAnimationReset += std::bind(&CMovementStartAnimation::endAnim, this); return true; } void CMovementStartAnimation::endAnim() { CBattleAnimation::endAnim(); delete this; } CReverseAnimation::CReverseAnimation(CBattleInterface * _owner, const CStack * stack, BattleHex dest, bool _priority) : CBattleStackAnimation(_owner, stack), hex(dest), priority(_priority) { logAnim->debug("Created reverse anim for %s", stack->getName()); } bool CReverseAnimation::init() { if(myAnim == nullptr || myAnim->isDead()) { endAnim(); return false; //there is no such creature } if(!priority && !isEarliest(false)) return false; if(myAnim->framesInGroup(CCreatureAnim::TURN_L)) { myAnim->setType(CCreatureAnim::TURN_L); myAnim->onAnimationReset += std::bind(&CReverseAnimation::setupSecondPart, this); } else { setupSecondPart(); } return true; } void CReverseAnimation::endAnim() { CBattleAnimation::endAnim(); if( stack->alive() )//don't do that if stack is dead myAnim->setType(CCreatureAnim::HOLDING); delete this; } void CReverseAnimation::rotateStack(CBattleInterface * owner, const CStack * stack, BattleHex hex) { owner->creDir[stack->ID] = !owner->creDir[stack->ID]; owner->creAnims[stack->ID]->pos = CClickableHex::getXYUnitAnim(hex, stack, owner); } void CReverseAnimation::setupSecondPart() { if(!stack) { endAnim(); return; } rotateStack(owner, stack, hex); if(myAnim->framesInGroup(CCreatureAnim::TURN_R)) { myAnim->setType(CCreatureAnim::TURN_R); myAnim->onAnimationReset += std::bind(&CReverseAnimation::endAnim, this); } else endAnim(); } CRangedAttackAnimation::CRangedAttackAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender) : CAttackAnimation(owner_, attacker, dest_, defender) { } CShootingAnimation::CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool _catapult, int _catapultDmg) : CRangedAttackAnimation(_owner, attacker, _dest, _attacked), catapultDamage(_catapultDmg) { logAnim->debug("Created shooting anim for %s", stack->getName()); } bool CShootingAnimation::init() { if( !CAttackAnimation::checkInitialConditions() ) return false; const CStack * shooter = attackingStack; if(!shooter || myAnim->isDead()) { endAnim(); return false; } //reverse unit if necessary if (attackingStack && attackedStack && owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStack->getPosition(), attackedStack->getPosition(), owner->creDir[attackingStack->ID], attackingStack->doubleWide(), owner->creDir[attackedStack->ID])) { owner->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->getPosition(), true)); return false; } // opponent must face attacker ( = different directions) before he can be attacked //FIXME: this cause freeze // if (attackingStack && attackedStack && // owner->creDir[attackingStack->ID] == owner->creDir[attackedStack->ID]) // return false; // Create the projectile animation //maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value) static const double straightAngle = 0.2; // Get further info about the shooter e.g. relative pos of projectile to unit. // If the creature id is 149 then it's a arrow tower which has no additional info so get the // actual arrow tower shooter instead. const CCreature *shooterInfo = shooter->getCreature(); if (shooterInfo->idNumber == CreatureID::ARROW_TOWERS) { int creID = owner->siegeH->town->town->clientInfo.siegeShooter; shooterInfo = CGI->creh->creatures[creID]; } ProjectileInfo spi; spi.shotDone = false; spi.creID = shooter->getCreature()->idNumber; spi.stackID = shooter->ID; // reverse if creature is facing right OR this is non-existing stack that is not tower (war machines) spi.reverse = attackingStack ? !owner->creDir[attackingStack->ID] : shooter->getCreature()->idNumber != CreatureID::ARROW_TOWERS ; spi.step = 0; spi.frameNum = 0; Point fromPos; Point destPos; // NOTE: two lines below return different positions (very notable with 2-hex creatures). Obtaining via creanims seems to be more precise fromPos = owner->creAnims[spi.stackID]->pos.topLeft(); //xycoord = CClickableHex::getXYUnitAnim(shooter->position, true, shooter, owner); destPos = CClickableHex::getXYUnitAnim(dest, attackedStack, owner); // to properly translate coordinates when shooter is rotated int multiplier = spi.reverse ? -1 : 1; double projectileAngle = atan2(fabs((double)destPos.y - fromPos.y), fabs((double)destPos.x - fromPos.x)); if(shooter->getPosition() < dest) projectileAngle = -projectileAngle; // Calculate projectile start position. Offsets are read out of the CRANIM.TXT. if (projectileAngle > straightAngle) { //upper shot spi.x = fromPos.x + 222 + ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier; spi.y = fromPos.y + 265 + shooterInfo->animation.upperRightMissleOffsetY; } else if (projectileAngle < -straightAngle) { //lower shot spi.x = fromPos.x + 222 + ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier; spi.y = fromPos.y + 265 + shooterInfo->animation.lowerRightMissleOffsetY; } else { //straight shot spi.x = fromPos.x + 222 + ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier; spi.y = fromPos.y + 265 + shooterInfo->animation.rightMissleOffsetY; } destPos += Point(225, 225); // recalculate angle taking in account offsets //projectileAngle = atan2(fabs(destPos.y - spi.y), fabs(destPos.x - spi.x)); //if(shooter->position < dest) // projectileAngle = -projectileAngle; if (attackedStack) { double animSpeed = AnimationControls::getProjectileSpeed(); // flight speed of projectile spi.lastStep = static_cast(sqrt(static_cast((destPos.x - spi.x) * (destPos.x - spi.x) + (destPos.y - spi.y) * (destPos.y - spi.y))) / animSpeed); if(spi.lastStep == 0) spi.lastStep = 1; spi.dx = (destPos.x - spi.x) / spi.lastStep; spi.dy = (destPos.y - spi.y) / spi.lastStep; } else { // Catapult attack spi.catapultInfo.reset(new CatapultProjectileInfo(Point(spi.x, spi.y), destPos)); double animSpeed = AnimationControls::getProjectileSpeed() / 10; spi.lastStep = std::abs((destPos.x - spi.x) / animSpeed); spi.dx = animSpeed; spi.dy = 0; auto img = owner->idToProjectile[spi.creID]->getImage(0); // Add explosion anim Point animPos(destPos.x - 126 + img->width() / 2, destPos.y - 105 + img->height() / 2); owner->addNewAnim( new CEffectAnimation(owner, catapultDamage ? "SGEXPL.DEF" : "CSGRCK.DEF", animPos.x, animPos.y)); } auto & angles = shooterInfo->animation.missleFrameAngles; double pi = boost::math::constants::pi(); if (owner->idToProjectile.count(spi.creID) == 0) //in some cases (known one: hero grants shooter bonus to unit) the shooter stack's projectile may not be properly initialized owner->initStackProjectile(shooter); // only frames below maxFrame are usable: anything higher is either no present or we don't know when it should be used size_t maxFrame = std::min(angles.size(), owner->idToProjectile.at(spi.creID)->size(0)); assert(maxFrame > 0); // values in angles array indicate position from which this frame was rendered, in degrees. // find frame that has closest angle to one that we need for this shot size_t bestID = 0; double bestDiff = fabs( angles[0] / 180 * pi - projectileAngle ); for (size_t i=1; ianimation.attackClimaxFrame; owner->projectiles.push_back(spi); //attack animation shooting = true; if(projectileAngle > straightAngle) group = CCreatureAnim::SHOOT_UP; else if(projectileAngle < -straightAngle) group = CCreatureAnim::SHOOT_DOWN; else //straight shot group = CCreatureAnim::SHOOT_FRONT; return true; } void CShootingAnimation::nextFrame() { for(auto & it : owner->pendingAnims) { CMovementStartAnimation * anim = dynamic_cast(it.first); CReverseAnimation * anim2 = dynamic_cast(it.first); if( (anim && anim->stack->ID == stack->ID) || (anim2 && anim2->stack->ID == stack->ID && anim2->priority ) ) return; } CAttackAnimation::nextFrame(); } void CShootingAnimation::endAnim() { // play wall hit/miss sound for catapult attack if(!attackedStack) { if(catapultDamage > 0) { CCS->soundh->playSound("WALLHIT"); } else { CCS->soundh->playSound("WALLMISS"); } } CAttackAnimation::endAnim(); delete this; } CCastAnimation::CCastAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender) : CRangedAttackAnimation(owner_, attacker, dest_, defender) { if(!dest_.isValid() && defender) dest = defender->getPosition(); } bool CCastAnimation::init() { if(!CAttackAnimation::checkInitialConditions()) return false; if(!attackingStack || myAnim->isDead()) { endAnim(); return false; } //reverse unit if necessary if(attackedStack) { if(owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStack->getPosition(), attackedStack->getPosition(), owner->creDir[attackingStack->ID], attackingStack->doubleWide(), owner->creDir[attackedStack->ID])) { owner->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->getPosition(), true)); return false; } } else { if(dest.isValid() && owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStack->getPosition(), dest, owner->creDir[attackingStack->ID], false, false)) { owner->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->getPosition(), true)); return false; } } //TODO: display spell projectile here static const double straightAngle = 0.2; Point fromPos; Point destPos; // NOTE: two lines below return different positions (very notable with 2-hex creatures). Obtaining via creanims seems to be more precise fromPos = owner->creAnims[attackingStack->ID]->pos.topLeft(); //xycoord = CClickableHex::getXYUnitAnim(shooter->getPosition(), true, shooter, owner); destPos = CClickableHex::getXYUnitAnim(dest, attackedStack, owner); double projectileAngle = atan2(fabs((double)destPos.y - fromPos.y), fabs((double)destPos.x - fromPos.x)); if(attackingStack->getPosition() < dest) projectileAngle = -projectileAngle; if(projectileAngle > straightAngle) group = CCreatureAnim::VCMI_CAST_UP; else if(projectileAngle < -straightAngle) group = CCreatureAnim::VCMI_CAST_DOWN; else group = CCreatureAnim::VCMI_CAST_FRONT; //fall back to H3 cast/2hex //even if creature have 2hex attack instead of cast it is ok since we fall back to attack anyway if(myAnim->framesInGroup(group) == 0) { if(projectileAngle > straightAngle) group = CCreatureAnim::CAST_UP; else if(projectileAngle < -straightAngle) group = CCreatureAnim::CAST_DOWN; else group = CCreatureAnim::CAST_FRONT; } //fall back to ranged attack if(myAnim->framesInGroup(group) == 0) { if(projectileAngle > straightAngle) group = CCreatureAnim::SHOOT_UP; else if(projectileAngle < -straightAngle) group = CCreatureAnim::SHOOT_DOWN; else group = CCreatureAnim::SHOOT_FRONT; } //fall back to normal attack if(myAnim->framesInGroup(group) == 0) { if(projectileAngle > straightAngle) group = CCreatureAnim::ATTACK_UP; else if(projectileAngle < -straightAngle) group = CCreatureAnim::ATTACK_DOWN; else group = CCreatureAnim::ATTACK_FRONT; } return true; } void CCastAnimation::nextFrame() { for(auto & it : owner->pendingAnims) { CReverseAnimation * anim = dynamic_cast(it.first); if(anim && anim->stack->ID == stack->ID && anim->priority) return; } if(myAnim->getType() != group) { myAnim->setType(group); myAnim->onAnimationReset += std::bind(&CAttackAnimation::endAnim, this); } CBattleAnimation::nextFrame(); } void CCastAnimation::endAnim() { CAttackAnimation::endAnim(); delete this; } CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx, int _dy, bool _Vflip, bool _alignToBottom) : CBattleAnimation(_owner), destTile(BattleHex::INVALID), x(_x), y(_y), dx(_dx), dy(_dy), Vflip(_Vflip), alignToBottom(_alignToBottom) { logAnim->debug("Created effect animation %s", _customAnim); customAnim = std::make_shared(_customAnim); } CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::shared_ptr _customAnim, int _x, int _y, int _dx, int _dy) : CBattleAnimation(_owner), destTile(BattleHex::INVALID), customAnim(_customAnim), x(_x), y(_y), dx(_dx), dy(_dy), Vflip(false), alignToBottom(false) { logAnim->debug("Created custom effect animation"); } CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip, bool _alignToBottom) : CBattleAnimation(_owner), destTile(_destTile), x(-1), y(-1), dx(0), dy(0), Vflip(_Vflip), alignToBottom(_alignToBottom) { logAnim->debug("Created effect animation %s", _customAnim); customAnim = std::make_shared(_customAnim); } bool CEffectAnimation::init() { if(!isEarliest(true)) return false; const bool areaEffect = (!destTile.isValid() && x == -1 && y == -1); std::shared_ptr animation = customAnim; animation->preload(); if(Vflip) animation->verticalFlip(); auto first = animation->getImage(0, 0, true); if(!first) { endAnim(); return false; } if(areaEffect) //f.e. armageddon { for(int i=0; i * first->width() < owner->pos.w ; ++i) { for(int j=0; j * first->height() < owner->pos.h ; ++j) { BattleEffect be; be.effectID = ID; be.animation = animation; be.currentFrame = 0; be.x = i * first->width() + owner->pos.x; be.y = j * first->height() + owner->pos.y; be.position = BattleHex::INVALID; owner->battleEffects.push_back(be); } } } else // Effects targeted at a specific creature/hex. { const CStack * destStack = owner->getCurrentPlayerInterface()->cb->battleGetStackByPos(destTile, false); BattleEffect be; be.effectID = ID; be.animation = animation; be.currentFrame = 0; //todo: lightning anim frame count override // if(effect == 1) // be.maxFrame = 3; be.x = x; be.y = y; if(destTile.isValid()) { Rect & tilePos = owner->bfield[destTile]->pos; if(x == -1) be.x = tilePos.x + tilePos.w/2 - first->width()/2; if(y == -1) { if(alignToBottom) be.y = tilePos.y + tilePos.h - first->height(); else be.y = tilePos.y - first->height()/2; } // Correction for 2-hex creatures. if(destStack != nullptr && destStack->doubleWide()) be.x += (destStack->side == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2; } assert(be.x != -1 && be.y != -1); //Indicate if effect should be drawn on top of everything or just on top of the hex be.position = destTile; owner->battleEffects.push_back(be); } //battleEffects return true; } void CEffectAnimation::nextFrame() { //notice: there may be more than one effect in owner->battleEffects correcponding to this animation (ie. armageddon) for(auto & elem : owner->battleEffects) { if(elem.effectID == ID) { elem.currentFrame += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000; if(elem.currentFrame >= elem.animation->size()) { endAnim(); break; } else { elem.x += dx; elem.y += dy; } } } } void CEffectAnimation::endAnim() { CBattleAnimation::endAnim(); std::vector::iterator> toDel; for(auto it = owner->battleEffects.begin(); it != owner->battleEffects.end(); ++it) { if(it->effectID == ID) { toDel.push_back(it); } } for(auto & elem : toDel) { owner->battleEffects.erase(elem); } delete this; } vcmi-0.99+git20190113.f06c8a87/client/battle/CBattleAnimations.h000066400000000000000000000174531342332007200234270ustar00rootroot00000000000000/* * CBattleAnimations.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/battle/BattleHex.h" #include "../widgets/Images.h" class CBattleInterface; class CStack; class CCreatureAnimation; struct CatapultProjectileInfo; struct StackAttackedInfo; /// Base class of battle animations class CBattleAnimation { protected: CBattleInterface * owner; public: virtual bool init() = 0; //to be called - if returned false, call again until returns true virtual void nextFrame() {} //call every new frame virtual void endAnim(); //to be called mostly internally; in this class it removes animation from pendingAnims list virtual ~CBattleAnimation(); bool isEarliest(bool perStackConcurrency); //determines if this animation is earliest of all ui32 ID; //unique identifier CBattleAnimation(CBattleInterface * _owner); }; /// Sub-class which is responsible for managing the battle stack animation. class CBattleStackAnimation : public CBattleAnimation { public: std::shared_ptr myAnim; //animation for our stack, managed by CBattleInterface const CStack * stack; //id of stack whose animation it is CBattleStackAnimation(CBattleInterface * _owner, const CStack * _stack); }; /// This class is responsible for managing the battle attack animation class CAttackAnimation : public CBattleStackAnimation { bool soundPlayed; protected: BattleHex dest; //attacked hex bool shooting; CCreatureAnim::EAnimType group; //if shooting is true, print this animation group const CStack *attackedStack; const CStack *attackingStack; int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature public: void nextFrame() override; void endAnim() override; bool checkInitialConditions(); CAttackAnimation(CBattleInterface *_owner, const CStack *attacker, BattleHex _dest, const CStack *defender); }; /// Animation of a defending unit class CDefenceAnimation : public CBattleStackAnimation { CCreatureAnim::EAnimType getMyAnimType(); std::string getMySound(); void startAnimation(); const CStack * attacker; //attacking stack bool rangedAttack; //if true, stack has been attacked by shooting bool killed; //if true, stack has been killed float timeToWait; // for how long this animation should be paused public: bool init() override; void nextFrame() override; void endAnim() override; CDefenceAnimation(StackAttackedInfo _attackedInfo, CBattleInterface * _owner); virtual ~CDefenceAnimation(){}; }; class CDummyAnimation : public CBattleAnimation { private: int counter; int howMany; public: bool init() override; void nextFrame() override; void endAnim() override; CDummyAnimation(CBattleInterface * _owner, int howManyFrames); virtual ~CDummyAnimation(){} }; /// Hand-to-hand attack class CMeleeAttackAnimation : public CAttackAnimation { public: bool init() override; void endAnim() override; CMeleeAttackAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked); virtual ~CMeleeAttackAnimation(){}; }; /// Move animation of a creature class CMovementAnimation : public CBattleStackAnimation { private: bool shouldRotate(); std::vector destTiles; //full path, includes already passed hexes ui32 curentMoveIndex; // index of nextHex in destTiles BattleHex oldPos; //position of stack before move double begX, begY; // starting position double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft double timeToMove; // full length of movement animation double progress; // range 0 -> 1, indicates move progrees. 0 = movement starts, 1 = move ends public: BattleHex nextHex; // next hex, to which creature move right now bool init() override; void nextFrame() override; void endAnim() override; CMovementAnimation(CBattleInterface *_owner, const CStack *_stack, std::vector _destTiles, int _distance); virtual ~CMovementAnimation(){}; }; /// Move end animation of a creature class CMovementEndAnimation : public CBattleStackAnimation { private: BattleHex destinationTile; public: bool init() override; void endAnim() override; CMovementEndAnimation(CBattleInterface * _owner, const CStack * _stack, BattleHex destTile); virtual ~CMovementEndAnimation(){}; }; /// Move start animation of a creature class CMovementStartAnimation : public CBattleStackAnimation { public: bool init() override; void endAnim() override; CMovementStartAnimation(CBattleInterface * _owner, const CStack * _stack); virtual ~CMovementStartAnimation(){}; }; /// Class responsible for animation of stack chaning direction (left <-> right) class CReverseAnimation : public CBattleStackAnimation { BattleHex hex; public: bool priority; //true - high, false - low bool init() override; static void rotateStack(CBattleInterface * owner, const CStack * stack, BattleHex hex); void setupSecondPart(); void endAnim() override; CReverseAnimation(CBattleInterface * _owner, const CStack * stack, BattleHex dest, bool _priority); virtual ~CReverseAnimation(){}; }; /// Small struct which contains information about the position and the velocity of a projectile struct ProjectileInfo { double x, y; //position on the screen double dx, dy; //change in position in one step int step, lastStep; //to know when finish showing this projectile int creID; //ID of creature that shot this projectile int stackID; //ID of stack int frameNum; //frame to display form projectile animation //bool spin; //if true, frameNum will be increased int animStartDelay; //frame of shooter animation when projectile should appear bool shotDone; // actual shot already done, projectile is flying bool reverse; //if true, projectile will be flipped by vertical asix std::shared_ptr catapultInfo; // holds info about the parabolic trajectory of the cannon }; class CRangedAttackAnimation : public CAttackAnimation { public: CRangedAttackAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender); protected: }; /// Shooting attack class CShootingAnimation : public CRangedAttackAnimation { private: int catapultDamage; public: bool init() override; void nextFrame() override; void endAnim() override; //last two params only for catapult attacks CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool _catapult = false, int _catapultDmg = 0); virtual ~CShootingAnimation(){}; }; class CCastAnimation : public CRangedAttackAnimation { public: CCastAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender); bool init() override; void nextFrame() override; void endAnim() override; }; /// This class manages effect animation class CEffectAnimation : public CBattleAnimation { private: BattleHex destTile; std::shared_ptr customAnim; int x, y, dx, dy; bool Vflip; bool alignToBottom; public: bool init() override; void nextFrame() override; void endAnim() override; CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx = 0, int _dy = 0, bool _Vflip = false, bool _alignToBottom = false); CEffectAnimation(CBattleInterface * _owner, std::shared_ptr _customAnim, int _x, int _y, int _dx = 0, int _dy = 0); CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip = false, bool _alignToBottom = false); virtual ~CEffectAnimation(){}; }; vcmi-0.99+git20190113.f06c8a87/client/battle/CBattleInterface.cpp000066400000000000000000003400621342332007200235530ustar00rootroot00000000000000/* * CBattleInterface.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CBattleInterface.h" #include "CBattleAnimations.h" #include "CBattleInterfaceClasses.h" #include "CCreatureAnimation.h" #include "../CBitmapHandler.h" #include "../CGameInfo.h" #include "../CMessage.h" #include "../CMT.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../CVideoHandler.h" #include "../Graphics.h" #include "../gui/CAnimation.h" #include "../gui/CCursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/SDL_Extensions.h" #include "../windows/CAdvmapInterface.h" #include "../windows/CCreatureWindow.h" #include "../windows/CSpellWindow.h" #include "../../CCallback.h" #include "../../lib/CStack.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/CondSh.h" #include "../../lib/CRandomGenerator.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CTownHandler.h" #include "../../lib/CGameState.h" #include "../../lib/mapping/CMap.h" #include "../../lib/NetPacks.h" #include "../../lib/UnlockGuard.h" CondSh CBattleInterface::animsAreDisplayed(false); CondSh CBattleInterface::givenCommand(nullptr); static void onAnimationFinished(const CStack *stack, std::weak_ptr anim) { if(anim.expired()) return; std::shared_ptr animation = anim.lock(); if (animation->isIdle()) { const CCreature *creature = stack->getCreature(); if (animation->framesInGroup(CCreatureAnim::MOUSEON) > 0) { if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10) animation->playOnce(CCreatureAnim::MOUSEON); else animation->setType(CCreatureAnim::HOLDING); } else { animation->setType(CCreatureAnim::HOLDING); } } // always reset callback animation->onAnimationReset += std::bind(&onAnimationFinished, stack, anim); } static void transformPalette(SDL_Surface *surf, double rCor, double gCor, double bCor) { SDL_Color *colorsToChange = surf->format->palette->colors; for (int g=0; gformat->palette->ncolors; ++g) { if ((colorsToChange+g)->b != 132 && (colorsToChange+g)->g != 231 && (colorsToChange+g)->r != 255) //it's not yellow border { (colorsToChange+g)->r = static_cast((colorsToChange+g)->r) *rCor; (colorsToChange+g)->g = static_cast((colorsToChange+g)->g) *gCor; (colorsToChange+g)->b = static_cast((colorsToChange+g)->b) *bCor; } } } void CBattleInterface::addNewAnim(CBattleAnimation *anim) { pendingAnims.push_back( std::make_pair(anim, false) ); animsAreDisplayed.setn(true); } CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt) : background(nullptr), attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0), activeStack(nullptr), mouseHoveredStack(nullptr), stackToActivate(nullptr), selectedStack(nullptr), previouslyHoveredHex(-1), currentlyHoveredHex(-1), attackingHex(-1), stackCanCastSpell(false), creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr), creatureSpellToCast(-1), siegeH(nullptr), attackerInt(att), defenderInt(defen), curInt(att), animIDhelper(0), myTurn(false), moveStarted(false), moveSoundHander(-1), bresult(nullptr) { OBJ_CONSTRUCTION; if(spectatorInt) { curInt = spectatorInt; } else if(!curInt) { //May happen when we are defending during network MP game -> attacker interface is just not present curInt = defenderInt; } animsAreDisplayed.setn(false); pos = myRect; strongInterest = true; givenCommand.setn(nullptr); //hot-seat -> check tactics for both players (defender may be local human) if(attackerInt && attackerInt->cb->battleGetTacticDist()) tacticianInterface = attackerInt; else if(defenderInt && defenderInt->cb->battleGetTacticDist()) tacticianInterface = defenderInt; //if we found interface of player with tactics, then enter tactics mode tacticsMode = static_cast(tacticianInterface); //create stack queue bool embedQueue; std::string queueSize = settings["battle"]["queueSize"].String(); if(queueSize == "auto") embedQueue = screen->h < 700; else embedQueue = screen->h < 700 || queueSize == "small"; queue = std::make_shared(embedQueue, this); if(!embedQueue) { if (settings["battle"]["showQueue"].Bool()) pos.y += queue->pos.h / 2; //center whole window queue->moveTo(Point(pos.x, pos.y - queue->pos.h)); } queue->update(); //preparing siege info const CGTownInstance *town = curInt->cb->battleGetDefendedTown(); if(town && town->hasFort()) { siegeH = new SiegeHelper(town, this); } CPlayerInterface::battleInt = this; //initializing armies this->army1 = army1; this->army2 = army2; std::vector stacks = curInt->cb->battleGetAllStacks(true); for(const CStack * s : stacks) { unitAdded(s); } //preparing menu background and terrain if(siegeH) { background = BitmapHandler::loadBitmap( siegeH->getSiegeName(0), false ); ui8 siegeLevel = curInt->cb->battleGetSiegeLevel(); if (siegeLevel >= 2) //citadel or castle { //print moat/mlip SDL_Surface *moat = BitmapHandler::loadBitmap( siegeH->getSiegeName(13) ), * mlip = BitmapHandler::loadBitmap( siegeH->getSiegeName(14) ); auto & info = siegeH->town->town->clientInfo; Point moatPos(info.siegePositions[13].x, info.siegePositions[13].y); Point mlipPos(info.siegePositions[14].x, info.siegePositions[14].y); if (moat) //eg. tower has no moat blitAt(moat, moatPos.x,moatPos.y, background); if (mlip) //eg. tower has no mlip blitAt(mlip, mlipPos.x, mlipPos.y, background); SDL_FreeSurface(moat); SDL_FreeSurface(mlip); } } else { auto bfieldType = (int)curInt->cb->battleGetBattlefieldType(); if (graphics->battleBacks.size() <= bfieldType || bfieldType < 0) logGlobal->error("%d is not valid battlefield type index!", bfieldType); else if (graphics->battleBacks[bfieldType].empty()) logGlobal->error("%d battlefield type does not have any backgrounds!", bfieldType); else { const std::string bgName = *RandomGeneratorUtil::nextItem(graphics->battleBacks[bfieldType], CRandomGenerator::getDefault()); background = BitmapHandler::loadBitmap(bgName, false); } } //preparing graphics for displaying amounts of creatures amountNormal = BitmapHandler::loadBitmap("CMNUMWIN.BMP"); CSDL_Ext::alphaTransform(amountNormal); transformPalette(amountNormal, 0.59, 0.19, 0.93); amountPositive = BitmapHandler::loadBitmap("CMNUMWIN.BMP"); CSDL_Ext::alphaTransform(amountPositive); transformPalette(amountPositive, 0.18, 1.00, 0.18); amountNegative = BitmapHandler::loadBitmap("CMNUMWIN.BMP"); CSDL_Ext::alphaTransform(amountNegative); transformPalette(amountNegative, 1.00, 0.18, 0.18); amountEffNeutral = BitmapHandler::loadBitmap("CMNUMWIN.BMP"); CSDL_Ext::alphaTransform(amountEffNeutral); transformPalette(amountEffNeutral, 1.00, 1.00, 0.18); //preparing buttons and console bOptions = std::make_shared(Point( 3, 561), "icm003.def", CGI->generaltexth->zelp[381], std::bind(&CBattleInterface::bOptionsf,this), SDLK_o); bSurrender = std::make_shared(Point( 54, 561), "icm001.def", CGI->generaltexth->zelp[379], std::bind(&CBattleInterface::bSurrenderf,this), SDLK_s); bFlee = std::make_shared(Point(105, 561), "icm002.def", CGI->generaltexth->zelp[380], std::bind(&CBattleInterface::bFleef,this), SDLK_r); bAutofight = std::make_shared(Point(157, 561), "icm004.def", CGI->generaltexth->zelp[382], std::bind(&CBattleInterface::bAutofightf,this), SDLK_a); bSpell = std::make_shared(Point(645, 561), "icm005.def", CGI->generaltexth->zelp[385], std::bind(&CBattleInterface::bSpellf,this), SDLK_c); bWait = std::make_shared(Point(696, 561), "icm006.def", CGI->generaltexth->zelp[386], std::bind(&CBattleInterface::bWaitf,this), SDLK_w); bDefence = std::make_shared(Point(747, 561), "icm007.def", CGI->generaltexth->zelp[387], std::bind(&CBattleInterface::bDefencef,this), SDLK_d); bDefence->assignedKeys.insert(SDLK_SPACE); bConsoleUp = std::make_shared(Point(624, 561), "ComSlide.def", std::make_pair("", ""), std::bind(&CBattleInterface::bConsoleUpf,this), SDLK_UP); bConsoleDown = std::make_shared(Point(624, 580), "ComSlide.def", std::make_pair("", ""), std::bind(&CBattleInterface::bConsoleDownf,this), SDLK_DOWN); bConsoleDown->setImageOrder(2, 3, 4, 5); console = std::make_shared(); console->pos.x += 211; console->pos.y += 560; console->pos.w = 406; console->pos.h = 38; if(tacticsMode) { btactNext = std::make_shared(Point(213, 560), "icm011.def", std::make_pair("", ""), [&](){ bTacticNextStack(nullptr);}, SDLK_SPACE); btactEnd = std::make_shared(Point(419, 560), "icm012.def", std::make_pair("", ""), [&](){ bEndTacticPhase();}, SDLK_RETURN); menu = BitmapHandler::loadBitmap("COPLACBR.BMP"); } else { menu = BitmapHandler::loadBitmap("CBAR.BMP"); } graphics->blueToPlayersAdv(menu, curInt->playerID); //loading hero animations if(hero1) // attacking hero { std::string battleImage; if(!hero1->type->battleImage.empty()) { battleImage = hero1->type->battleImage; } else { if(hero1->sex) battleImage = hero1->type->heroClass->imageBattleFemale; else battleImage = hero1->type->heroClass->imageBattleMale; } attackingHero = std::make_shared(battleImage, false, hero1->tempOwner, hero1->tempOwner == curInt->playerID ? hero1 : nullptr, this); auto img = attackingHero->animation->getImage(0, 0, true); if(img) attackingHero->pos = genRect(img->height(), img->width(), pos.x - 43, pos.y - 19); } if(hero2) // defending hero { std::string battleImage; if(!hero2->type->battleImage.empty()) { battleImage = hero2->type->battleImage; } else { if(hero2->sex) battleImage = hero2->type->heroClass->imageBattleFemale; else battleImage = hero2->type->heroClass->imageBattleMale; } defendingHero = std::make_shared(battleImage, true, hero2->tempOwner, hero2->tempOwner == curInt->playerID ? hero2 : nullptr, this); auto img = defendingHero->animation->getImage(0, 0, true); if(img) defendingHero->pos = genRect(img->height(), img->width(), pos.x + 693, pos.y - 19); } //preparing cells and hexes cellBorder = BitmapHandler::loadBitmap("CCELLGRD.BMP"); CSDL_Ext::alphaTransform(cellBorder); cellShade = BitmapHandler::loadBitmap("CCELLSHD.BMP"); CSDL_Ext::alphaTransform(cellShade); for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h) { auto hex = std::make_shared(); hex->myNumber = h; hex->pos = hexPosition(h); hex->accessible = true; hex->myInterface = this; bfield.push_back(hex); } //locking occupied positions on batlefield for(const CStack * s : stacks) //stacks gained at top of this function if(s->initialPosition >= 0) //turrets have position < 0 bfield[s->getPosition()]->accessible = false; //preparing graphic with cell borders cellBorders = CSDL_Ext::newSurface(background->w, background->h, cellBorder); //copying palette for (int g=0; gformat->palette->ncolors; ++g) //we assume that cellBorders->format->palette->ncolors == 256 { cellBorders->format->palette->colors[g] = cellBorder->format->palette->colors[g]; } //palette copied for (int i=0; iw; ++cellX) { for (int cellY = 0; cellY < cellBorder->h; ++cellY) { if (y+cellY < cellBorders->h && x+cellX < cellBorders->w) * ((Uint8*)cellBorders->pixels + (y+cellY) *cellBorders->pitch + (x+cellX)) |= *((Uint8*)cellBorder->pixels + cellY *cellBorder->pitch + cellX); } } } } backgroundWithHexes = CSDL_Ext::newSurface(background->w, background->h, screen); //preparing obstacle defs auto obst = curInt->cb->battleGetAllObstacles(); for(auto & elem : obst) { if(elem->obstacleType == CObstacleInstance::USUAL) { std::string animationName = elem->getInfo().defName; auto cached = animationsCache.find(animationName); if(cached == animationsCache.end()) { auto animation = std::make_shared(animationName); animationsCache[animationName] = animation; obstacleAnimations[elem->uniqueID] = animation; animation->preload(); } else { obstacleAnimations[elem->uniqueID] = cached->second; } } else if (elem->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { std::string animationName = elem->getInfo().defName; auto cached = animationsCache.find(animationName); if(cached == animationsCache.end()) { auto animation = std::make_shared(); animation->setCustom(animationName, 0, 0); animationsCache[animationName] = animation; obstacleAnimations[elem->uniqueID] = animation; animation->preload(); } else { obstacleAnimations[elem->uniqueID] = cached->second; } } } for(auto hex : bfield) addChild(hex.get()); if (tacticsMode) bTacticNextStack(); CCS->musich->stopMusic(); int channel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); auto onIntroPlayed = []() { if (LOCPLINT->battleInt) CCS->musich->playMusicFromSet("battle", true); }; CCS->soundh->setCallback(channel, onIntroPlayed); memset(stackCountOutsideHexes, 1, GameConstants::BFIELD_SIZE *sizeof(bool)); //initialize array with trues currentAction = INVALID; selectedAction = INVALID; addUsedEvents(RCLICK | MOVE | KEYBOARD); blockUI(settings["session"]["spectate"].Bool()); } CBattleInterface::~CBattleInterface() { CPlayerInterface::battleInt = nullptr; givenCommand.cond.notify_all(); //that two lines should make any activeStack waiting thread to finish if (active) //dirty fix for #485 { deactivate(); } SDL_FreeSurface(background); SDL_FreeSurface(menu); SDL_FreeSurface(amountNormal); SDL_FreeSurface(amountNegative); SDL_FreeSurface(amountPositive); SDL_FreeSurface(amountEffNeutral); SDL_FreeSurface(cellBorders); SDL_FreeSurface(backgroundWithHexes); SDL_FreeSurface(cellBorder); SDL_FreeSurface(cellShade); delete siegeH; //TODO: play AI tracks if battle was during AI turn //if (!curInt->makingTurn) //CCS->musich->playMusicFromSet(CCS->musich->aiMusics, -1); if (adventureInt && adventureInt->selection) { int terrain = LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType; CCS->musich->playMusicFromSet("terrain", terrain, true); } animsAreDisplayed.setn(false); } void CBattleInterface::setPrintCellBorders(bool set) { Settings cellBorders = settings.write["battle"]["cellBorders"]; cellBorders->Bool() = set; redrawBackgroundWithHexes(activeStack); GH.totalRedraw(); } void CBattleInterface::setPrintStackRange(bool set) { Settings stackRange = settings.write["battle"]["stackRange"]; stackRange->Bool() = set; redrawBackgroundWithHexes(activeStack); GH.totalRedraw(); } void CBattleInterface::setPrintMouseShadow(bool set) { Settings shadow = settings.write["battle"]["mouseShadow"]; shadow->Bool() = set; } void CBattleInterface::activate() { if (curInt->isAutoFightOn) { bAutofight->activate(); return; } CIntObject::activate(); bOptions->activate(); bSurrender->activate(); bFlee->activate(); bAutofight->activate(); bSpell->activate(); bWait->activate(); bDefence->activate(); for (auto hex : bfield) hex->activate(); if (attackingHero) attackingHero->activate(); if (defendingHero) defendingHero->activate(); if (settings["battle"]["showQueue"].Bool()) queue->activate(); if (tacticsMode) { btactNext->activate(); btactEnd->activate(); } else { bConsoleUp->activate(); bConsoleDown->activate(); } LOCPLINT->cingconsole->activate(); } void CBattleInterface::deactivate() { CIntObject::deactivate(); bOptions->deactivate(); bSurrender->deactivate(); bFlee->deactivate(); bAutofight->deactivate(); bSpell->deactivate(); bWait->deactivate(); bDefence->deactivate(); for (auto hex : bfield) hex->deactivate(); if (attackingHero) attackingHero->deactivate(); if (defendingHero) defendingHero->deactivate(); if (settings["battle"]["showQueue"].Bool()) queue->deactivate(); if (tacticsMode) { btactNext->deactivate(); btactEnd->deactivate(); } else { bConsoleUp->deactivate(); bConsoleDown->deactivate(); } LOCPLINT->cingconsole->deactivate(); } void CBattleInterface::keyPressed(const SDL_KeyboardEvent & key) { if (key.keysym.sym == SDLK_q && key.state == SDL_PRESSED) { if (settings["battle"]["showQueue"].Bool()) //hide queue hideQueue(); else showQueue(); } else if (key.keysym.sym == SDLK_f && key.state == SDL_PRESSED) { enterCreatureCastingMode(); } else if (key.keysym.sym == SDLK_ESCAPE) { endCastingSpell(); } } void CBattleInterface::mouseMoved(const SDL_MouseMotionEvent &sEvent) { auto hexItr = std::find_if(bfield.begin(), bfield.end(), [](std::shared_ptr hex) { return hex->hovered && hex->strictHovered; }); handleHex(hexItr == bfield.end() ? -1 : (*hexItr)->myNumber, MOVE); } void CBattleInterface::setBattleCursor(const int myNumber) { const CClickableHex & hoveredHex = *bfield[myNumber]; CCursorHandler *cursor = CCS->curh; const double subdividingAngle = 2.0*M_PI/6.0; // Divide a hex into six sectors. const double hexMidX = hoveredHex.pos.x + hoveredHex.pos.w/2.0; const double hexMidY = hoveredHex.pos.y + hoveredHex.pos.h/2.0; const double cursorHexAngle = M_PI - atan2(hexMidY - cursor->ypos, cursor->xpos - hexMidX) + subdividingAngle/2; //TODO: refactor this nightmare const double sector = fmod(cursorHexAngle/subdividingAngle, 6.0); const int zigzagCorrection = !((myNumber/GameConstants::BFIELD_WIDTH)%2); // Off-by-one correction needed to deal with the odd battlefield rows. std::vector sectorCursor; // From left to bottom left. sectorCursor.push_back(8); sectorCursor.push_back(9); sectorCursor.push_back(10); sectorCursor.push_back(11); sectorCursor.push_back(12); sectorCursor.push_back(7); const bool doubleWide = activeStack->doubleWide(); bool aboveAttackable = true, belowAttackable = true; // Exclude directions which cannot be attacked from. // Check to the left. if (myNumber%GameConstants::BFIELD_WIDTH <= 1 || !vstd::contains(occupyableHexes, myNumber - 1)) { sectorCursor[0] = -1; } // Check top left, top right as well as above for 2-hex creatures. if (myNumber/GameConstants::BFIELD_WIDTH == 0) { sectorCursor[1] = -1; sectorCursor[2] = -1; aboveAttackable = false; } else { if (doubleWide) { bool attackRow[4] = {true, true, true, true}; if (myNumber%GameConstants::BFIELD_WIDTH <= 1 || !vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH - 2 + zigzagCorrection)) attackRow[0] = false; if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection)) attackRow[1] = false; if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH + zigzagCorrection)) attackRow[2] = false; if (myNumber%GameConstants::BFIELD_WIDTH >= GameConstants::BFIELD_WIDTH - 2 || !vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH + 1 + zigzagCorrection)) attackRow[3] = false; if (!(attackRow[0] && attackRow[1])) sectorCursor[1] = -1; if (!(attackRow[1] && attackRow[2])) aboveAttackable = false; if (!(attackRow[2] && attackRow[3])) sectorCursor[2] = -1; } else { if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection)) sectorCursor[1] = -1; if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH + zigzagCorrection)) sectorCursor[2] = -1; } } // Check to the right. if (myNumber%GameConstants::BFIELD_WIDTH >= GameConstants::BFIELD_WIDTH - 2 || !vstd::contains(occupyableHexes, myNumber + 1)) { sectorCursor[3] = -1; } // Check bottom right, bottom left as well as below for 2-hex creatures. if (myNumber/GameConstants::BFIELD_WIDTH == GameConstants::BFIELD_HEIGHT - 1) { sectorCursor[4] = -1; sectorCursor[5] = -1; belowAttackable = false; } else { if (doubleWide) { bool attackRow[4] = {true, true, true, true}; if (myNumber%GameConstants::BFIELD_WIDTH <= 1 || !vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH - 2 + zigzagCorrection)) attackRow[0] = false; if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection)) attackRow[1] = false; if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH + zigzagCorrection)) attackRow[2] = false; if (myNumber%GameConstants::BFIELD_WIDTH >= GameConstants::BFIELD_WIDTH - 2 || !vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH + 1 + zigzagCorrection)) attackRow[3] = false; if (!(attackRow[0] && attackRow[1])) sectorCursor[5] = -1; if (!(attackRow[1] && attackRow[2])) belowAttackable = false; if (!(attackRow[2] && attackRow[3])) sectorCursor[4] = -1; } else { if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH + zigzagCorrection)) sectorCursor[4] = -1; if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection)) sectorCursor[5] = -1; } } // Determine index from sector. int cursorIndex; if (doubleWide) { sectorCursor.insert(sectorCursor.begin() + 5, belowAttackable ? 13 : -1); sectorCursor.insert(sectorCursor.begin() + 2, aboveAttackable ? 14 : -1); if (sector < 1.5) cursorIndex = sector; else if (sector >= 1.5 && sector < 2.5) cursorIndex = 2; else if (sector >= 2.5 && sector < 4.5) cursorIndex = (int) sector + 1; else if (sector >= 4.5 && sector < 5.5) cursorIndex = 6; else cursorIndex = (int) sector + 2; } else { cursorIndex = sector; } // Generally should NEVER happen, but to avoid the possibility of having endless loop below... [#1016] if (!vstd::contains_if (sectorCursor, [](int sc) { return sc != -1; })) { logGlobal->error("Error: for hex %d cannot find a hex to attack from!", myNumber); attackingHex = -1; return; } // Find the closest direction attackable, starting with the right one. // FIXME: Is this really how the original H3 client does it? int i = 0; while (sectorCursor[(cursorIndex + i)%sectorCursor.size()] == -1) //Why hast thou forsaken me? i = i <= 0 ? 1 - i : -i; // 0, 1, -1, 2, -2, 3, -3 etc.. int index = (cursorIndex + i)%sectorCursor.size(); //hopefully we get elements from sectorCursor cursor->changeGraphic(ECursor::COMBAT, sectorCursor[index]); switch (index) { case 0: attackingHex = myNumber - 1; //left break; case 1: attackingHex = myNumber - GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection; //top left break; case 2: attackingHex = myNumber - GameConstants::BFIELD_WIDTH + zigzagCorrection; //top right break; case 3: attackingHex = myNumber + 1; //right break; case 4: attackingHex = myNumber + GameConstants::BFIELD_WIDTH + zigzagCorrection; //bottom right break; case 5: attackingHex = myNumber + GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection; //bottom left break; } BattleHex hex(attackingHex); if (!hex.isValid()) attackingHex = -1; } void CBattleInterface::clickRight(tribool down, bool previousState) { if (!down) { endCastingSpell(); } } void CBattleInterface::bOptionsf() { if (spellDestSelectMode) //we are casting a spell return; CCS->curh->changeGraphic(ECursor::ADVENTURE,0); Rect tempRect = genRect(431, 481, 160, 84); tempRect += pos.topLeft(); GH.pushIntT(tempRect, this); } void CBattleInterface::bSurrenderf() { if (spellDestSelectMode) //we are casting a spell return; int cost = curInt->cb->battleGetSurrenderCost(); if (cost >= 0) { std::string enemyHeroName = curInt->cb->battleGetEnemyHero().name; if (enemyHeroName.empty()) enemyHeroName = "#ENEMY#"; //TODO: should surrendering without enemy hero be enabled? std::string surrenderMessage = boost::str(boost::format(CGI->generaltexth->allTexts[32]) % enemyHeroName % cost); //%s states: "I will accept your surrender and grant you and your troops safe passage for the price of %d gold." curInt->showYesNoDialog(surrenderMessage, [this](){ reallySurrender(); }, nullptr); } } void CBattleInterface::bFleef() { if (spellDestSelectMode) //we are casting a spell return; if ( curInt->cb->battleCanFlee() ) { CFunctionList ony = std::bind(&CBattleInterface::reallyFlee,this); curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat? } else { std::vector> comps; std::string heroName; //calculating fleeing hero's name if (attackingHeroInstance) if (attackingHeroInstance->tempOwner == curInt->cb->getMyColor()) heroName = attackingHeroInstance->name; if (defendingHeroInstance) if (defendingHeroInstance->tempOwner == curInt->cb->getMyColor()) heroName = defendingHeroInstance->name; //calculating text auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present. %s can not retreat! //printing message curInt->showInfoDialog(boost::to_string(txt), comps); } } void CBattleInterface::reallyFlee() { giveCommand(EActionType::RETREAT); CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); } void CBattleInterface::reallySurrender() { if (curInt->cb->getResourceAmount(Res::GOLD) < curInt->cb->battleGetSurrenderCost()) { curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold! } else { giveCommand(EActionType::SURRENDER); CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); } } void CBattleInterface::bAutofightf() { if (spellDestSelectMode) //we are casting a spell return; //Stop auto-fight mode if (curInt->isAutoFightOn) { assert(curInt->autofightingAI); curInt->isAutoFightOn = false; logGlobal->trace("Stopping the autofight..."); } else { curInt->isAutoFightOn = true; blockUI(true); auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); ai->init(curInt->cb); ai->battleStart(army1, army2, int3(0,0,0), attackingHeroInstance, defendingHeroInstance, curInt->cb->battleGetMySide()); curInt->autofightingAI = ai; curInt->cb->registerBattleInterface(ai); requestAutofightingAIToTakeAction(); } } void CBattleInterface::bSpellf() { if (spellDestSelectMode) //we are casting a spell return; if (!myTurn) return; auto myHero = currentHero(); if(!myHero) return; CCS->curh->changeGraphic(ECursor::ADVENTURE,0); ESpellCastProblem::ESpellCastProblem spellCastProblem = curInt->cb->battleCanCastSpell(myHero, spells::Mode::HERO); if(spellCastProblem == ESpellCastProblem::OK) { GH.pushIntT(myHero, curInt.get()); } else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED) { //TODO: move to spell mechanics, add more information to spell cast problem //Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible auto blockingBonus = currentHero()->getBonusLocalFirst(Selector::type(Bonus::BLOCK_ALL_MAGIC)); if (!blockingBonus) return; if (blockingBonus->source == Bonus::ARTIFACT) { const int artID = blockingBonus->sid; //If we have artifact, put name of our hero. Otherwise assume it's the enemy. //TODO check who *really* is source of bonus std::string heroName = myHero->hasArt(artID) ? myHero->name : enemyHero().name; //%s wields the %s, an ancient artifact which creates a p dead to all magic. LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683]) % heroName % CGI->arth->artifacts[artID]->Name())); } } } void CBattleInterface::bWaitf() { if (spellDestSelectMode) //we are casting a spell return; if (activeStack != nullptr) giveCommand(EActionType::WAIT); } void CBattleInterface::bDefencef() { if (spellDestSelectMode) //we are casting a spell return; if (activeStack != nullptr) giveCommand(EActionType::DEFEND); } void CBattleInterface::bConsoleUpf() { if (spellDestSelectMode) //we are casting a spell return; console->scrollUp(); } void CBattleInterface::bConsoleDownf() { if (spellDestSelectMode) //we are casting a spell return; console->scrollDown(); } void CBattleInterface::unitAdded(const CStack * stack) { creDir[stack->ID] = stack->side == BattleSide::ATTACKER; // must be set before getting stack position Point coords = CClickableHex::getXYUnitAnim(stack->getPosition(), stack, this); if(stack->initialPosition < 0) //turret { const CCreature *turretCreature = CGI->creh->creatures[siegeH->town->town->clientInfo.siegeShooter]; creAnims[stack->ID] = AnimationControls::getAnimation(turretCreature); // Turret positions are read out of the config/wall_pos.txt int posID = 0; switch (stack->initialPosition) { case -2: // keep creature posID = 18; break; case -3: // bottom creature posID = 19; break; case -4: // upper creature posID = 20; break; } if (posID != 0) { coords.x = siegeH->town->town->clientInfo.siegePositions[posID].x + this->pos.x; coords.y = siegeH->town->town->clientInfo.siegePositions[posID].y + this->pos.y; } creAnims[stack->ID]->pos.h = 225; } else { creAnims[stack->ID] = AnimationControls::getAnimation(stack->getCreature()); creAnims[stack->ID]->onAnimationReset += std::bind(&onAnimationFinished, stack, creAnims[stack->ID]); creAnims[stack->ID]->pos.h = creAnims[stack->ID]->getHeight(); } creAnims[stack->ID]->pos.x = coords.x; creAnims[stack->ID]->pos.y = coords.y; creAnims[stack->ID]->pos.w = creAnims[stack->ID]->getWidth(); creAnims[stack->ID]->setType(CCreatureAnim::HOLDING); //loading projectiles for units if(stack->isShooter()) { initStackProjectile(stack); } } void CBattleInterface::initStackProjectile(const CStack * stack) { const CCreature * creature;//creature whose shots should be loaded if(stack->getCreature()->idNumber == CreatureID::ARROW_TOWERS) creature = CGI->creh->creatures[siegeH->town->town->clientInfo.siegeShooter]; else creature = stack->getCreature(); std::shared_ptr projectile = std::make_shared(creature->animation.projectileImageName); projectile->preload(); if(projectile->size(1) != 0) logAnim->error("Expected empty group 1 in stack projectile"); else projectile->createFlippedGroup(0, 1); idToProjectile[stack->getCreature()->idNumber] = projectile; } void CBattleInterface::stackRemoved(uint32_t stackID) { if (activeStack != nullptr) { if (activeStack->ID == stackID) { BattleAction *action = new BattleAction(); action->side = defendingHeroInstance ? (curInt->playerID == defendingHeroInstance->tempOwner) : false; action->actionType = EActionType::CANCEL; action->stackNumber = activeStack->ID; givenCommand.setn(action); setActiveStack(nullptr); } } //todo: ensure that ghost stack animation has fadeout effect redrawBackgroundWithHexes(activeStack); queue->update(); } void CBattleInterface::stackActivated(const CStack *stack) //TODO: check it all before game state is changed due to abilities { stackToActivate = stack; waitForAnims(); if (stackToActivate) //during waiting stack may have gotten activated through show activateStack(); } void CBattleInterface::stackMoved(const CStack *stack, std::vector destHex, int distance) { addNewAnim(new CMovementAnimation(this, stack, destHex, distance)); waitForAnims(); } void CBattleInterface::stacksAreAttacked(std::vector attackedInfos, const std::vector & battleLog) { for(auto & attackedInfo : attackedInfos) { //if (!attackedInfo.cloneKilled) //FIXME: play dead animation for cloned creature before it vanishes addNewAnim(new CDefenceAnimation(attackedInfo, this)); if(attackedInfo.rebirth) { displayEffect(50, attackedInfo.defender->getPosition()); //TODO: play reverse death animation CCS->soundh->playSound(soundBase::RESURECT); } } waitForAnims(); std::array killedBySide = {0, 0}; int targets = 0, damage = 0; for(const StackAttackedInfo & attackedInfo : attackedInfos) { ++targets; damage += attackedInfo.dmg; ui8 side = attackedInfo.defender->side; killedBySide.at(side) += attackedInfo.amountKilled; } int killed = killedBySide[0] + killedBySide[1]; for(ui8 side = 0; side < 2; side++) { if(killedBySide.at(side) > killedBySide.at(1-side)) setHeroAnimation(side, 2); else if(killedBySide.at(side) < killedBySide.at(1-side)) setHeroAnimation(side, 3); } for (auto & attackedInfo : attackedInfos) { if (attackedInfo.rebirth) creAnims[attackedInfo.defender->ID]->setType(CCreatureAnim::HOLDING); if (attackedInfo.cloneKilled) stackRemoved(attackedInfo.defender->ID); } if(!battleLog.empty()) displayBattleLog(battleLog); else printConsoleAttacked(attackedInfos.front().defender, damage, killed, attackedInfos.front().attacker, (targets > 1)); //creatures perish } void CBattleInterface::stackAttacking( const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting ) { if (shooting) { addNewAnim(new CShootingAnimation(this, attacker, dest, attacked)); } else { addNewAnim(new CMeleeAttackAnimation(this, attacker, dest, attacked)); } //waitForAnims(); } void CBattleInterface::newRoundFirst( int round ) { waitForAnims(); } void CBattleInterface::newRound(int number) { console->addText(CGI->generaltexth->allTexts[412]); } void CBattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional) { const CStack * actor = nullptr; if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER) { actor = activeStack; } auto side = curInt->cb->playerToSide(curInt->playerID); if(!side) { logGlobal->error("Player %s is not in battle", curInt->playerID.getStr()); return; } auto ba = new BattleAction(); //is deleted in CPlayerInterface::activeStack() ba->side = side.get(); ba->actionType = action; ba->aimToHex(tile); ba->actionSubtype = additional; sendCommand(ba, actor); } void CBattleInterface::sendCommand(BattleAction *& command, const CStack * actor) { command->stackNumber = actor ? actor->unitId() : ((command->side == BattleSide::ATTACKER) ? -1 : -2); if(!tacticsMode) { logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero")); myTurn = false; setActiveStack(nullptr); givenCommand.setn(command); } else { curInt->cb->battleMakeTacticAction(command); vstd::clear_pointer(command); setActiveStack(nullptr); //next stack will be activated when action ends } } bool CBattleInterface::isTileAttackable(const BattleHex & number) const { for (auto & elem : occupyableHexes) { if (BattleHex::mutualPosition(elem, number) != -1 || elem == number) return true; } return false; } bool CBattleInterface::isCatapultAttackable(BattleHex hex) const { if (!siegeH || tacticsMode) return false; auto wallPart = curInt->cb->battleHexToWallPart(hex); if (!curInt->cb->isWallPartPotentiallyAttackable(wallPart)) return false; auto state = curInt->cb->battleGetWallState(static_cast(wallPart)); return state != EWallState::DESTROYED && state != EWallState::NONE; } const CGHeroInstance * CBattleInterface::getActiveHero() { const CStack *attacker = activeStack; if(!attacker) { return nullptr; } if(attacker->side == BattleSide::ATTACKER) { return attackingHeroInstance; } return defendingHeroInstance; } void CBattleInterface::hexLclicked(int whichOne) { handleHex(whichOne, LCLICK); } void CBattleInterface::stackIsCatapulting(const CatapultAttack & ca) { if (ca.attacker != -1) { const CStack *stack = curInt->cb->battleGetStackByID(ca.attacker); for (auto attackInfo : ca.attackedParts) { addNewAnim(new CShootingAnimation(this, stack, attackInfo.destinationTile, nullptr, true, attackInfo.damageDealt)); } } else { //no attacker stack, assume spell-related (earthquake) - only hit animation for (auto attackInfo : ca.attackedParts) { Point destPos = CClickableHex::getXYUnitAnim(attackInfo.destinationTile, nullptr, this) + Point(99, 120); addNewAnim(new CEffectAnimation(this, "SGEXPL.DEF", destPos.x, destPos.y)); } } waitForAnims(); for (auto attackInfo : ca.attackedParts) { int wallId = attackInfo.attackedPart + 2; //gate state changing handled separately if (wallId == SiegeHelper::GATE) continue; SDL_FreeSurface(siegeH->walls[wallId]); siegeH->walls[wallId] = BitmapHandler::loadBitmap( siegeH->getSiegeName(wallId, curInt->cb->battleGetWallState(attackInfo.attackedPart))); } } void CBattleInterface::battleFinished(const BattleResult& br) { bresult = &br; { auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); animsAreDisplayed.waitUntil(false); } setActiveStack(nullptr); displayBattleFinished(); } void CBattleInterface::displayBattleFinished() { CCS->curh->changeGraphic(ECursor::ADVENTURE,0); if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool()) { close(); return; } GH.pushInt(std::make_shared(*bresult, *(this->curInt))); curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897 CPlayerInterface::battleInt = nullptr; } void CBattleInterface::spellCast(const BattleSpellCast * sc) { const SpellID spellID = sc->spellID; const CSpell * spell = spellID.toSpell(); if(!spell) return; const std::string & castSoundPath = spell->getCastSound(); if (!castSoundPath.empty()) CCS->soundh->playSound(castSoundPath); const auto casterStackID = sc->casterStack; const CStack * casterStack = nullptr; if(casterStackID >= 0) { casterStack = curInt->cb->battleGetStackByID(casterStackID); } Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos; //hero position by default { if(casterStack != nullptr) { srccoord = CClickableHex::getXYUnitAnim(casterStack->getPosition(), casterStack, this); srccoord.x += 250; srccoord.y += 240; } } if(casterStack != nullptr && sc->activeCast) { //todo: custom cast animation for hero displaySpellCast(spellID, casterStack->getPosition()); addNewAnim(new CCastAnimation(this, casterStack, sc->tile, curInt->cb->battleGetStackByPos(sc->tile))); } waitForAnims(); //wait for cast animation //playing projectile animation if (sc->tile.isValid()) { Point destcoord = CClickableHex::getXYUnitAnim(sc->tile, curInt->cb->battleGetStackByPos(sc->tile), this); //position attacked by projectile destcoord.x += 250; destcoord.y += 240; //animation angle double angle = atan2(static_cast(destcoord.x - srccoord.x), static_cast(destcoord.y - srccoord.y)); bool Vflip = (angle < 0); if (Vflip) angle = -angle; std::string animToDisplay = spell->animationInfo.selectProjectile(angle); if(!animToDisplay.empty()) { //TODO: calculate inside CEffectAnimation std::shared_ptr tmp = std::make_shared(animToDisplay); tmp->load(0, 0); auto first = tmp->getImage(0, 0); //displaying animation double diffX = (destcoord.x - srccoord.x)*(destcoord.x - srccoord.x); double diffY = (destcoord.y - srccoord.y)*(destcoord.y - srccoord.y); double distance = sqrt(diffX + diffY); int steps = distance / AnimationControls::getSpellEffectSpeed() + 1; int dx = (destcoord.x - srccoord.x - first->width())/steps; int dy = (destcoord.y - srccoord.y - first->height())/steps; addNewAnim(new CEffectAnimation(this, animToDisplay, srccoord.x, srccoord.y, dx, dy, Vflip)); } } waitForAnims(); //wait for projectile animation displaySpellHit(spellID, sc->tile); //queuing affect animation for(auto & elem : sc->affectedCres) { auto stack = curInt->cb->battleGetStackByID(elem, false); if(stack) displaySpellEffect(spellID, stack->getPosition()); } //queuing additional animation for(auto & elem : sc->customEffects) { auto stack = curInt->cb->battleGetStackByID(elem.stack, false); if(stack) displayEffect(elem.effect, stack->getPosition()); } //displaying message in console displayBattleLog(sc->battleLog); waitForAnims(); //mana absorption if (sc->manaGained > 0) { Point leftHero = Point(15, 30) + pos; Point rightHero = Point(755, 30) + pos; addNewAnim(new CEffectAnimation(this, sc->side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero.x, leftHero.y, 0, 0, false)); addNewAnim(new CEffectAnimation(this, sc->side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero.x, rightHero.y, 0, 0, false)); } } void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) { for(const MetaString & line : sse.battleLog) console->addText(line.toString()); if(activeStack != nullptr) redrawBackgroundWithHexes(activeStack); } CBattleInterface::PossibleActions CBattleInterface::getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const { PossibleActions spellSelMode = ANY_LOCATION; const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode); if(ti.massive || ti.type == spells::AimType::NO_TARGET) spellSelMode = NO_LOCATION; else if(ti.type == spells::AimType::LOCATION && ti.clearAffected) spellSelMode = FREE_LOCATION; else if(ti.type == spells::AimType::CREATURE) spellSelMode = AIMED_SPELL_CREATURE; else if(ti.type == spells::AimType::OBSTACLE) spellSelMode = OBSTACLE; return spellSelMode; } void CBattleInterface::setHeroAnimation(ui8 side, int phase) { if(side == BattleSide::ATTACKER) { if(attackingHero) attackingHero->setPhase(phase); } else { if(defendingHero) defendingHero->setPhase(phase); } } void CBattleInterface::castThisSpell(SpellID spellID) { spellToCast = std::make_shared(); spellToCast->actionType = EActionType::HERO_SPELL; spellToCast->actionSubtype = spellID; //spell number spellToCast->stackNumber = (attackingHeroInstance->tempOwner == curInt->playerID) ? -1 : -2; spellToCast->side = defendingHeroInstance ? (curInt->playerID == defendingHeroInstance->tempOwner) : false; spellDestSelectMode = true; creatureCasting = false; //choosing possible targets const CGHeroInstance *castingHero = (attackingHeroInstance->tempOwner == curInt->playerID) ? attackingHeroInstance : defendingHeroInstance; assert(castingHero); // code below assumes non-null hero sp = spellID.toSpell(); PossibleActions spellSelMode = getCasterAction(sp, castingHero, spells::Mode::HERO); if (spellSelMode == NO_LOCATION) //user does not have to select location { spellToCast->aimToHex(BattleHex::INVALID); curInt->cb->battleMakeAction(spellToCast.get()); endCastingSpell(); } else { possibleActions.clear(); possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment GH.fakeMouseMove();//update cursor } } void CBattleInterface::displayBattleLog(const std::vector & battleLog) { for(const auto & line : battleLog) { std::string formatted = line.toString(); boost::algorithm::trim(formatted); if(!console->addText(formatted)) logGlobal->warn("Too long battle log line"); } } void CBattleInterface::displayCustomEffects(const std::vector & customEffects) { for(const CustomEffectInfo & one : customEffects) { if(one.sound != 0) CCS->soundh->playSound(soundBase::soundID(one.sound)); const CStack * s = curInt->cb->battleGetStackByID(one.stack, false); if(s && one.effect != 0) displayEffect(one.effect, s->getPosition()); } } void CBattleInterface::displayEffect(ui32 effect, BattleHex destTile) { std::string customAnim = graphics->battleACToDef[effect][0]; addNewAnim(new CEffectAnimation(this, customAnim, destTile)); } void CBattleInterface::displaySpellAnimation(const CSpell::TAnimation & animation, BattleHex destinationTile) { if (animation.pause > 0) { addNewAnim(new CDummyAnimation(this, animation.pause)); } else { addNewAnim(new CEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM)); } } void CBattleInterface::displaySpellCast(SpellID spellID, BattleHex destinationTile) { const CSpell *spell = spellID.toSpell(); if (spell == nullptr) return; for (const CSpell::TAnimation & animation : spell->animationInfo.cast) { displaySpellAnimation(animation, destinationTile); } } void CBattleInterface::displaySpellEffect(SpellID spellID, BattleHex destinationTile) { const CSpell *spell = spellID.toSpell(); if (spell == nullptr) return; for (const CSpell::TAnimation & animation : spell->animationInfo.affect) { displaySpellAnimation(animation, destinationTile); } } void CBattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTile) { const CSpell *spell = spellID.toSpell(); if (spell == nullptr) return; for (const CSpell::TAnimation & animation : spell->animationInfo.hit) { displaySpellAnimation(animation, destinationTile); } } void CBattleInterface::battleTriggerEffect(const BattleTriggerEffect & bte) { const CStack * stack = curInt->cb->battleGetStackByID(bte.stackID); if(!stack) { logGlobal->error("Invalid stack ID %d", bte.stackID); return; } //don't show animation when no HP is regenerated switch(bte.effect) { //TODO: move to bonus type handler case Bonus::HP_REGENERATION: displayEffect(74, stack->getPosition()); CCS->soundh->playSound(soundBase::REGENER); break; case Bonus::MANA_DRAIN: displayEffect(77, stack->getPosition()); CCS->soundh->playSound(soundBase::MANADRAI); break; case Bonus::POISON: displayEffect(67, stack->getPosition()); CCS->soundh->playSound(soundBase::POISON); break; case Bonus::FEAR: displayEffect(15, stack->getPosition()); CCS->soundh->playSound(soundBase::FEAR); break; case Bonus::MORALE: { std::string hlp = CGI->generaltexth->allTexts[33]; boost::algorithm::replace_first(hlp,"%s",(stack->getName())); displayEffect(20,stack->getPosition()); CCS->soundh->playSound(soundBase::GOODMRLE); console->addText(hlp); break; } default: return; } //waitForAnims(); //fixme: freezes game :? } void CBattleInterface::setAnimSpeed(int set) { Settings speed = settings.write["battle"]["animationSpeed"]; speed->Float() = float(set) / 100; } int CBattleInterface::getAnimSpeed() const { if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-battle-speed"].isNull()) return vstd::round(settings["session"]["spectate-battle-speed"].Float() *100); return vstd::round(settings["battle"]["animationSpeed"].Float() *100); } CPlayerInterface *CBattleInterface::getCurrentPlayerInterface() const { return curInt.get(); } void CBattleInterface::setActiveStack(const CStack *stack) { if (activeStack) // update UI creAnims[activeStack->ID]->setBorderColor(AnimationControls::getNoBorder()); activeStack = stack; if (activeStack) // update UI creAnims[activeStack->ID]->setBorderColor(AnimationControls::getGoldBorder()); blockUI(activeStack == nullptr); } void CBattleInterface::setHoveredStack(const CStack *stack) { if (mouseHoveredStack) creAnims[mouseHoveredStack->ID]->setBorderColor(AnimationControls::getNoBorder()); // stack must be alive and not active (which uses gold border instead) if (stack && stack->alive() && stack != activeStack) { mouseHoveredStack = stack; if (mouseHoveredStack) { creAnims[mouseHoveredStack->ID]->setBorderColor(AnimationControls::getBlueBorder()); if (creAnims[mouseHoveredStack->ID]->framesInGroup(CCreatureAnim::MOUSEON) > 0) creAnims[mouseHoveredStack->ID]->playOnce(CCreatureAnim::MOUSEON); } } else mouseHoveredStack = nullptr; } void CBattleInterface::activateStack() { myTurn = true; if (!!attackerInt && defenderInt) //hotseat -> need to pick which interface "takes over" as active curInt = attackerInt->playerID == stackToActivate->owner ? attackerInt : defenderInt; setActiveStack(stackToActivate); stackToActivate = nullptr; const CStack *s = activeStack; queue->update(); redrawBackgroundWithHexes(activeStack); //set casting flag to true if creature can use it to not check it every time const auto spellcaster = s->getBonusLocalFirst(Selector::type(Bonus::SPELLCASTER)), randomSpellcaster = s->getBonusLocalFirst(Selector::type(Bonus::RANDOM_SPELLCASTER)); if(s->canCast() && (spellcaster || randomSpellcaster)) { stackCanCastSpell = true; if(randomSpellcaster) creatureSpellToCast = -1; //spell will be set later on cast else creatureSpellToCast = curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), s, CBattleInfoCallback::RANDOM_AIMED); //faerie dragon can cast only one spell until their next move //TODO: what if creature can cast BOTH random genie spell and aimed spell? //TODO: faerie dragon type spell should be selected by server } else { stackCanCastSpell = false; creatureSpellToCast = -1; } getPossibleActionsForStack(s, false); GH.fakeMouseMove(); } void CBattleInterface::endCastingSpell() { if(spellDestSelectMode) { spellToCast.reset(); sp = nullptr; spellDestSelectMode = false; CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); if(activeStack) { getPossibleActionsForStack(activeStack, false); //restore actions after they were cleared myTurn = true; } } else { if(activeStack) { getPossibleActionsForStack(activeStack, false); GH.fakeMouseMove(); } } } void CBattleInterface::enterCreatureCastingMode() { //silently check for possible errors if (!myTurn) return; if (tacticsMode) return; //hero is casting a spell if (spellDestSelectMode) return; if (!activeStack) return; if (!stackCanCastSpell) return; //random spellcaster if (creatureSpellToCast == -1) return; if (vstd::contains(possibleActions, NO_LOCATION)) { const spells::Caster *caster = activeStack; const CSpell *spell = SpellID(creatureSpellToCast).toSpell(); const bool isCastingPossible = spell->canBeCastAt(curInt->cb.get(), spells::Mode::CREATURE_ACTIVE, caster, BattleHex::INVALID); if (isCastingPossible) { myTurn = false; giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, creatureSpellToCast); selectedStack = nullptr; CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); } } else { getPossibleActionsForStack(activeStack, true); GH.fakeMouseMove(); } } void CBattleInterface::getPossibleActionsForStack(const CStack *stack, const bool forceCast) { possibleActions.clear(); if (tacticsMode) { possibleActions.push_back(MOVE_TACTICS); possibleActions.push_back(CHOOSE_TACTICS_STACK); } else { PossibleActions notPriority = INVALID; //first action will be prioritized over later ones if(stack->canCast()) //TODO: check for battlefield effects that prevent casting? { if(stack->hasBonusOfType (Bonus::SPELLCASTER)) { if(creatureSpellToCast != -1) { const CSpell *spell = SpellID(creatureSpellToCast).toSpell(); PossibleActions act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE); if(forceCast) { //forced action to be only one possible possibleActions.push_back(act); return; } else //if cast is not forced, cast action will have lowest priority notPriority = act; } } if (stack->hasBonusOfType (Bonus::RANDOM_SPELLCASTER)) possibleActions.push_back (RANDOM_GENIE_SPELL); if (stack->hasBonusOfType (Bonus::DAEMON_SUMMONING)) possibleActions.push_back (RISE_DEMONS); } if(stack->canShoot()) possibleActions.push_back(SHOOT); if(stack->hasBonusOfType(Bonus::RETURN_AFTER_STRIKE)) possibleActions.push_back(ATTACK_AND_RETURN); possibleActions.push_back(ATTACK); //all active stacks can attack possibleActions.push_back(WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere if (stack->canMove() && stack->Speed(0, true)) //probably no reason to try move war machines or bound stacks possibleActions.push_back (MOVE_STACK); //all active stacks can attack if (siegeH && stack->hasBonusOfType (Bonus::CATAPULT)) //TODO: check shots possibleActions.push_back (CATAPULT); if (stack->hasBonusOfType (Bonus::HEALER)) possibleActions.push_back (HEAL); if (notPriority != INVALID) possibleActions.push_back(notPriority); } } void CBattleInterface::printConsoleAttacked(const CStack * defender, int dmg, int killed, const CStack * attacker, bool multiple) { std::string formattedText; if(attacker) //ignore if stacks were killed by spell { MetaString text; attacker->addText(text, MetaString::GENERAL_TXT, 376); attacker->addNameReplacement(text); text.addReplacement(dmg); formattedText = text.toString(); } if(killed > 0) { if(attacker) formattedText.append(" "); boost::format txt; if(killed > 1) { txt = boost::format(CGI->generaltexth->allTexts[379]) % killed % (multiple ? CGI->generaltexth->allTexts[43] : defender->getCreature()->namePl); // creatures perish } else //killed == 1 { txt = boost::format(CGI->generaltexth->allTexts[378]) % (multiple ? CGI->generaltexth->allTexts[42] : defender->getCreature()->nameSing); // creature perishes } std::string trimmed = boost::to_string(txt); boost::algorithm::trim(trimmed); // these default h3 texts have unnecessary new lines, so get rid of them before displaying formattedText.append(trimmed); } console->addText(formattedText); } void CBattleInterface::endAction(const BattleAction* action) { const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); if(action->actionType == EActionType::HERO_SPELL) setHeroAnimation(action->side, 0); //check if we should reverse stacks //for some strange reason, it's not enough TStacks stacks = curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY); for (const CStack *s : stacks) { if (s && creDir[s->ID] != (s->side == BattleSide::ATTACKER) && s->alive() && creAnims[s->ID]->isIdle()) { addNewAnim(new CReverseAnimation(this, s, s->getPosition(), false)); } } queue->update(); if (tacticsMode) //stack ended movement in tactics phase -> select the next one bTacticNextStack(stack); if(action->actionType == EActionType::HERO_SPELL) //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed redrawBackgroundWithHexes(activeStack); if (activeStack && !animsAreDisplayed.get() && pendingAnims.empty() && !active) { logGlobal->warn("Something wrong... interface was deactivated but there is no animation. Reactivating..."); blockUI(false); } else { // block UI if no active stack (e.g. enemy turn); blockUI(activeStack == nullptr); } } void CBattleInterface::hideQueue() { Settings showQueue = settings.write["battle"]["showQueue"]; showQueue->Bool() = false; queue->deactivate(); if (!queue->embedded) { moveBy(Point(0, -queue->pos.h / 2)); GH.totalRedraw(); } } void CBattleInterface::showQueue() { Settings showQueue = settings.write["battle"]["showQueue"]; showQueue->Bool() = true; queue->activate(); if (!queue->embedded) { moveBy(Point(0, +queue->pos.h / 2)); GH.totalRedraw(); } } void CBattleInterface::blockUI(bool on) { bool canCastSpells = false; auto hero = curInt->cb->battleGetMyHero(); if(hero) { ESpellCastProblem::ESpellCastProblem spellcastingProblem = curInt->cb->battleCanCastSpell(hero, spells::Mode::HERO); //if magic is blocked, we leave button active, so the message can be displayed after button click canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED; } bool canWait = activeStack ? !activeStack->waited() : false; bOptions->block(on); bFlee->block(on || !curInt->cb->battleCanFlee()); bSurrender->block(on || curInt->cb->battleGetSurrenderCost() < 0); // block only if during enemy turn and auto-fight is off // othervice - crash on accessing non-exisiting active stack bAutofight->block(!curInt->isAutoFightOn && !activeStack); if (tacticsMode && btactEnd && btactNext) { btactNext->block(on); btactEnd->block(on); } else { bConsoleUp->block(on); bConsoleDown->block(on); } bSpell->block(on || tacticsMode || !canCastSpells); bWait->block(on || tacticsMode || !canWait); bDefence->block(on || tacticsMode); } void CBattleInterface::startAction(const BattleAction* action) { //setActiveStack(nullptr); setHoveredStack(nullptr); blockUI(true); if(action->actionType == EActionType::END_TACTIC_PHASE) { SDL_FreeSurface(menu); menu = BitmapHandler::loadBitmap("CBAR.bmp"); graphics->blueToPlayersAdv(menu, curInt->playerID); return; } const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); if (stack) { queue->update(); } else { assert(action->actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number } auto actionTarget = action->getTarget(curInt->cb.get()); if(action->actionType == EActionType::WALK || (action->actionType == EActionType::WALK_AND_ATTACK && actionTarget.at(0).hexValue != stack->getPosition())) { assert(stack); moveStarted = true; if (creAnims[action->stackNumber]->framesInGroup(CCreatureAnim::MOVE_START)) { pendingAnims.push_back(std::make_pair(new CMovementStartAnimation(this, stack), false)); } } redraw(); // redraw after deactivation, including proper handling of hovered hexes if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell { setHeroAnimation(action->side, 4); return; } if (!stack) { logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action->stackNumber); return; } int txtid = 0; switch(action->actionType) { case EActionType::WAIT: txtid = 136; break; case EActionType::BAD_MORALE: txtid = -34; //negative -> no separate singular/plural form displayEffect(30, stack->getPosition()); CCS->soundh->playSound(soundBase::BADMRLE); break; } if(txtid != 0) console->addText(stack->formatGeneralMessage(txtid)); //displaying special abilities switch(action->actionType) { case EActionType::STACK_HEAL: displayEffect(74, actionTarget.at(0).hexValue); CCS->soundh->playSound(soundBase::REGENER); break; } } void CBattleInterface::waitForAnims() { auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); animsAreDisplayed.waitWhileTrue(); } void CBattleInterface::bEndTacticPhase() { setActiveStack(nullptr); blockUI(true); tacticsMode = false; } static bool immobile(const CStack *s) { return !s->Speed(0, true); //should bound stacks be immobile? } void CBattleInterface::bTacticNextStack(const CStack * current) { if (!current) current = activeStack; //no switching stacks when the current one is moving waitForAnims(); TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE); vstd::erase_if (stacksOfMine, &immobile); if (stacksOfMine.empty()) { bEndTacticPhase(); return; } auto it = vstd::find(stacksOfMine, current); if (it != stacksOfMine.end() && ++it != stacksOfMine.end()) stackActivated(*it); else stackActivated(stacksOfMine.front()); } std::string formatDmgRange(std::pair dmgRange) { if (dmgRange.first != dmgRange.second) return (boost::format("%d - %d") % dmgRange.first % dmgRange.second).str(); else return (boost::format("%d") % dmgRange.first).str(); } bool CBattleInterface::canStackMoveHere(const CStack * activeStack, BattleHex myNumber) { std::vector acc = curInt->cb->battleGetAvailableHexes(activeStack); BattleHex shiftedDest = myNumber.cloneInDirection(activeStack->destShiftDir(), false); if (vstd::contains(acc, myNumber)) return true; else if (activeStack->doubleWide() && vstd::contains(acc, shiftedDest)) return true; else return false; } void CBattleInterface::handleHex(BattleHex myNumber, int eventType) { if (!myTurn) //we are not permit to do anything return; // This function handles mouse move over hexes and l-clicking on them. // First we decide what happens if player clicks on this hex and set appropriately // consoleMsg, cursorFrame/Type and prepare lambda realizeAction. // // Then, depending whether it was hover/click we either call the action or set tooltip/cursor. //used when hovering -> tooltip message and cursor to be set std::string consoleMsg; bool setCursor = true; //if we want to suppress setting cursor ECursor::ECursorTypes cursorType = ECursor::COMBAT; int cursorFrame = ECursor::COMBAT_POINTER; //TODO: is this line used? //used when l-clicking -> action to be called upon the click std::function realizeAction; //Get stack on the hex - first try to grab the alive one, if not found -> allow dead stacks. const CStack * shere = curInt->cb->battleGetStackByPos(myNumber, true); if(!shere) shere = curInt->cb->battleGetStackByPos(myNumber, false); if(!activeStack) return; bool ourStack = false; if (shere) ourStack = shere->owner == curInt->playerID; //stack changed, update selection border if (shere != mouseHoveredStack) { setHoveredStack(shere); } localActions.clear(); illegalActions.clear(); const bool forcedAction = possibleActions.size() == 1; for (PossibleActions action : possibleActions) { bool legalAction = false; //this action is legal and can be performed bool notLegal = false; //this action is not legal and should display message switch (action) { case CHOOSE_TACTICS_STACK: if (shere && ourStack) legalAction = true; break; case MOVE_TACTICS: case MOVE_STACK: { if (!(shere && shere->alive())) //we can walk on dead stacks { if(canStackMoveHere(activeStack, myNumber)) legalAction = true; } break; } case ATTACK: case WALK_AND_ATTACK: case ATTACK_AND_RETURN: { if(curInt->cb->battleCanAttack(activeStack, shere, myNumber)) { if (isTileAttackable(myNumber)) // move isTileAttackable to be part of battleCanAttack? { setBattleCursor(myNumber); // temporary - needed for following function :( BattleHex attackFromHex = fromWhichHexAttack(myNumber); if (attackFromHex >= 0) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) legalAction = true; } } } break; case SHOOT: if(curInt->cb->battleCanShoot(activeStack, myNumber)) legalAction = true; break; case ANY_LOCATION: if (myNumber > -1) //TODO: this should be checked for all actions { if(isCastingPossibleHere(activeStack, shere, myNumber)) legalAction = true; } break; case AIMED_SPELL_CREATURE: if(shere && isCastingPossibleHere(activeStack, shere, myNumber)) legalAction = true; break; case RANDOM_GENIE_SPELL: { if(shere && ourStack && shere != activeStack && shere->alive()) //only positive spells for other allied creatures { int spellID = curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), shere, CBattleInfoCallback::RANDOM_GENIE); if(spellID > -1) { legalAction = true; } } } break; case OBSTACLE: if(isCastingPossibleHere(activeStack, shere, myNumber)) legalAction = true; break; case TELEPORT: { //todo: move to mechanics ui8 skill = 0; if (creatureCasting) skill = activeStack->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell()); else skill = getActiveHero()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell()); //TODO: explicitely save power, skill if (curInt->cb->battleCanTeleportTo(selectedStack, myNumber, skill)) legalAction = true; else notLegal = true; } break; case SACRIFICE: //choose our living stack to sacrifice if (shere && shere != selectedStack && ourStack && shere->alive()) legalAction = true; else notLegal = true; break; case FREE_LOCATION: legalAction = true; if(!isCastingPossibleHere(activeStack, shere, myNumber)) { legalAction = false; notLegal = true; } break; case CATAPULT: if (isCatapultAttackable(myNumber)) legalAction = true; break; case HEAL: if (shere && ourStack && shere->canBeHealed()) legalAction = true; break; case RISE_DEMONS: if (shere && ourStack && !shere->alive()) { if (!(shere->hasBonusOfType(Bonus::UNDEAD) || shere->hasBonusOfType(Bonus::NON_LIVING) || shere->summoned || shere->isClone() || shere->hasBonusOfType(Bonus::SIEGE_WEAPON) )) legalAction = true; } break; } if (legalAction) localActions.push_back (action); else if (notLegal || forcedAction) illegalActions.push_back (action); } illegalAction = INVALID; //clear it in first place if (vstd::contains(localActions, selectedAction)) //try to use last selected action by default currentAction = selectedAction; else if (localActions.size()) //if not possible, select first available action (they are sorted by suggested priority) currentAction = localActions.front(); else //no legal action possible { currentAction = INVALID; //don't allow to do anything if (vstd::contains(illegalActions, selectedAction)) illegalAction = selectedAction; else if (illegalActions.size()) illegalAction = illegalActions.front(); else if (shere && ourStack && shere->alive()) //last possibility - display info about our creature { currentAction = CREATURE_INFO; } else illegalAction = INVALID; //we should never be here } bool isCastingPossible = false; bool secondaryTarget = false; if (currentAction > INVALID) { switch (currentAction) //display console message, realize selected action { case CHOOSE_TACTICS_STACK: consoleMsg = (boost::format(CGI->generaltexth->allTexts[481]) % shere->getName()).str(); //Select %s realizeAction = [=](){ stackActivated(shere); }; break; case MOVE_TACTICS: case MOVE_STACK: if (activeStack->hasBonusOfType(Bonus::FLYING)) { cursorFrame = ECursor::COMBAT_FLY; consoleMsg = (boost::format(CGI->generaltexth->allTexts[295]) % activeStack->getName()).str(); //Fly %s here } else { cursorFrame = ECursor::COMBAT_MOVE; consoleMsg = (boost::format(CGI->generaltexth->allTexts[294]) % activeStack->getName()).str(); //Move %s here } realizeAction = [=]() { if(activeStack->doubleWide()) { std::vector acc = curInt->cb->battleGetAvailableHexes(activeStack); BattleHex shiftedDest = myNumber.cloneInDirection(activeStack->destShiftDir(), false); if(vstd::contains(acc, myNumber)) giveCommand(EActionType::WALK, myNumber); else if(vstd::contains(acc, shiftedDest)) giveCommand(EActionType::WALK, shiftedDest); } else { giveCommand(EActionType::WALK, myNumber); } }; break; case ATTACK: case WALK_AND_ATTACK: case ATTACK_AND_RETURN: //TODO: allow to disable return { setBattleCursor(myNumber); //handle direction of cursor and attackable tile setCursor = false; //don't overwrite settings from the call above //TODO: what does it mean? bool returnAfterAttack = currentAction == ATTACK_AND_RETURN; realizeAction = [=]() { BattleHex attackFromHex = fromWhichHexAttack(myNumber); if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) { auto command = new BattleAction(BattleAction::makeMeleeAttack(activeStack, myNumber, attackFromHex, returnAfterAttack)); sendCommand(command, activeStack); } }; std::string estDmgText = formatDmgRange(curInt->cb->battleEstimateDamage(activeStack, shere)); //calculating estimated dmg consoleMsg = (boost::format(CGI->generaltexth->allTexts[36]) % shere->getName() % estDmgText).str(); //Attack %s (%s damage) } break; case SHOOT: { if (curInt->cb->battleHasShootingPenalty(activeStack, myNumber)) cursorFrame = ECursor::COMBAT_SHOOT_PENALTY; else cursorFrame = ECursor::COMBAT_SHOOT; realizeAction = [=](){giveCommand(EActionType::SHOOT, myNumber);}; std::string estDmgText = formatDmgRange(curInt->cb->battleEstimateDamage(activeStack, shere)); //calculating estimated dmg //printing - Shoot %s (%d shots left, %s damage) consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % activeStack->shots.available() % estDmgText).str(); } break; case AIMED_SPELL_CREATURE: sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[27]) % sp->name % shere->getName()); //Cast %s on %s switch (sp->id) { case SpellID::SACRIFICE: case SpellID::TELEPORT: selectedStack = shere; //remember first target secondaryTarget = true; break; } isCastingPossible = true; break; case ANY_LOCATION: sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s isCastingPossible = true; break; case RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell sp = nullptr; consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[301]) % shere->getName()); //Cast a spell on % creatureCasting = true; isCastingPossible = true; break; case TELEPORT: consoleMsg = CGI->generaltexth->allTexts[25]; //Teleport Here cursorFrame = ECursor::COMBAT_TELEPORT; isCastingPossible = true; break; case OBSTACLE: consoleMsg = CGI->generaltexth->allTexts[550]; //TODO: remove obstacle cursor isCastingPossible = true; break; case SACRIFICE: consoleMsg = (boost::format(CGI->generaltexth->allTexts[549]) % shere->getName()).str(); //sacrifice the %s cursorFrame = ECursor::COMBAT_SACRIFICE; isCastingPossible = true; break; case FREE_LOCATION: consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s isCastingPossible = true; break; case HEAL: cursorFrame = ECursor::COMBAT_HEAL; consoleMsg = (boost::format(CGI->generaltexth->allTexts[419]) % shere->getName()).str(); //Apply first aid to the %s realizeAction = [=](){ giveCommand(EActionType::STACK_HEAL, myNumber); }; //command healing break; case RISE_DEMONS: cursorType = ECursor::SPELLBOOK; realizeAction = [=]() { giveCommand(EActionType::DAEMON_SUMMONING, myNumber); }; break; case CATAPULT: cursorFrame = ECursor::COMBAT_SHOOT_CATAPULT; realizeAction = [=](){ giveCommand(EActionType::CATAPULT, myNumber); }; break; case CREATURE_INFO: { cursorFrame = ECursor::COMBAT_QUERY; consoleMsg = (boost::format(CGI->generaltexth->allTexts[297]) % shere->getName()).str(); realizeAction = [=](){ GH.pushIntT(shere, false); }; break; } } } else //no possible valid action, display message { switch (illegalAction) { case AIMED_SPELL_CREATURE: case RANDOM_GENIE_SPELL: cursorFrame = ECursor::COMBAT_BLOCKED; consoleMsg = CGI->generaltexth->allTexts[23]; break; case TELEPORT: cursorFrame = ECursor::COMBAT_BLOCKED; consoleMsg = CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination break; case SACRIFICE: consoleMsg = CGI->generaltexth->allTexts[543]; //choose army to sacrifice break; case FREE_LOCATION: cursorFrame = ECursor::COMBAT_BLOCKED; consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[181]) % sp->name); //No room to place %s here break; default: if (myNumber == -1) CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); //set neutral cursor over menu etc. else cursorFrame = ECursor::COMBAT_BLOCKED; break; } } if (isCastingPossible) //common part { switch (currentAction) //don't use that with teleport / sacrifice { case TELEPORT: //FIXME: more generic solution? case SACRIFICE: break; default: cursorType = ECursor::SPELLBOOK; cursorFrame = 0; if (consoleMsg.empty() && sp) consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s break; } realizeAction = [=]() { if(secondaryTarget) //select that target now { possibleActions.clear(); switch (sp->id.toEnum()) { case SpellID::TELEPORT: //don't cast spell yet, only select target spellToCast->aimToUnit(shere); possibleActions.push_back(TELEPORT); break; case SpellID::SACRIFICE: spellToCast->aimToHex(myNumber); possibleActions.push_back(SACRIFICE); break; } } else { if (creatureCasting) { if (sp) { giveCommand(EActionType::MONSTER_SPELL, myNumber, creatureSpellToCast); } else //unknown random spell { giveCommand(EActionType::MONSTER_SPELL, myNumber); } } else { assert(sp); switch (sp->id.toEnum()) { case SpellID::SACRIFICE: spellToCast->aimToUnit(shere);//victim break; default: spellToCast->aimToHex(myNumber); break; } curInt->cb->battleMakeAction(spellToCast.get()); endCastingSpell(); } selectedStack = nullptr; } }; } { if (eventType == MOVE) { if (setCursor) CCS->curh->changeGraphic(cursorType, cursorFrame); this->console->alterText(consoleMsg); this->console->whoSetAlter = 0; } if (eventType == LCLICK && realizeAction) { //opening creature window shouldn't affect myTurn... if ((currentAction != CREATURE_INFO) && !secondaryTarget) { myTurn = false; //tends to crash with empty calls } realizeAction(); if (!secondaryTarget) //do not replace teleport or sacrifice cursor CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); this->console->alterText(""); } } } bool CBattleInterface::isCastingPossibleHere(const CStack *sactive, const CStack *shere, BattleHex myNumber) { creatureCasting = stackCanCastSpell && !spellDestSelectMode; //TODO: allow creatures to cast aimed spells bool isCastingPossible = true; int spellID = -1; if (creatureCasting) { if (creatureSpellToCast > -1 && (shere != sactive)) //can't cast on itself spellID = creatureSpellToCast; //TODO: merge with SpellTocast? } else //hero casting { spellID = spellToCast->actionSubtype; } sp = nullptr; if (spellID >= 0) sp = CGI->spellh->objects[spellID]; if (sp) { const spells::Caster *caster = creatureCasting ? static_cast(sactive) : static_cast(curInt->cb->battleGetMyHero()); if (caster == nullptr) { isCastingPossible = false;//just in case } else { const spells::Mode mode = creatureCasting ? spells::Mode::CREATURE_ACTIVE : spells::Mode::HERO; isCastingPossible = sp->canBeCastAt(curInt->cb.get(), mode, caster, myNumber); } } else isCastingPossible = false; if (!myNumber.isAvailable() && !shere) //empty tile outside battlefield (or in the unavailable border column) isCastingPossible = false; return isCastingPossible; } BattleHex CBattleInterface::fromWhichHexAttack(BattleHex myNumber) { //TODO far too much repeating code BattleHex destHex; switch(CCS->curh->frame) { case 12: //from bottom right { bool doubleWide = activeStack->doubleWide(); destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH+1 ) + (activeStack->side == BattleSide::ATTACKER && doubleWide ? 1 : 0); if(vstd::contains(occupyableHexes, destHex)) return destHex; else if(activeStack->side == BattleSide::ATTACKER) { if (vstd::contains(occupyableHexes, destHex+1)) return destHex+1; } else //if we are defender { if(vstd::contains(occupyableHexes, destHex-1)) return destHex-1; } break; } case 7: //from bottom left { destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH-1 : GameConstants::BFIELD_WIDTH ); if (vstd::contains(occupyableHexes, destHex)) return destHex; else if(activeStack->side == BattleSide::ATTACKER) { if(vstd::contains(occupyableHexes, destHex+1)) return destHex+1; } else //we are defender { if(vstd::contains(occupyableHexes, destHex-1)) return destHex-1; } break; } case 8: //from left { if(activeStack->doubleWide() && activeStack->side == BattleSide::DEFENDER) { std::vector acc = curInt->cb->battleGetAvailableHexes(activeStack); if (vstd::contains(acc, myNumber)) return myNumber - 1; else return myNumber - 2; } else { return myNumber - 1; } break; } case 9: //from top left { destHex = myNumber - ((myNumber/GameConstants::BFIELD_WIDTH) % 2 ? GameConstants::BFIELD_WIDTH + 1 : GameConstants::BFIELD_WIDTH); if(vstd::contains(occupyableHexes, destHex)) return destHex; else if(activeStack->side == BattleSide::ATTACKER) { if(vstd::contains(occupyableHexes, destHex+1)) return destHex+1; } else //if we are defender { if(vstd::contains(occupyableHexes, destHex-1)) return destHex-1; } break; } case 10: //from top right { bool doubleWide = activeStack->doubleWide(); destHex = myNumber - ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH-1 ) + (activeStack->side == BattleSide::ATTACKER && doubleWide ? 1 : 0); if(vstd::contains(occupyableHexes, destHex)) return destHex; else if(activeStack->side == BattleSide::ATTACKER) { if(vstd::contains(occupyableHexes, destHex+1)) return destHex+1; } else //if we are defender { if(vstd::contains(occupyableHexes, destHex-1)) return destHex-1; } break; } case 11: //from right { if(activeStack->doubleWide() && activeStack->side == BattleSide::ATTACKER) { std::vector acc = curInt->cb->battleGetAvailableHexes(activeStack); if(vstd::contains(acc, myNumber)) return myNumber + 1; else return myNumber + 2; } else { return myNumber + 1; } break; } case 13: //from bottom { destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH+1 ); if(vstd::contains(occupyableHexes, destHex)) return destHex; else if(activeStack->side == BattleSide::ATTACKER) { if(vstd::contains(occupyableHexes, destHex+1)) return destHex+1; } else //if we are defender { if(vstd::contains(occupyableHexes, destHex-1)) return destHex-1; } break; } case 14: //from top { destHex = myNumber - ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH-1 ); if (vstd::contains(occupyableHexes, destHex)) return destHex; else if(activeStack->side == BattleSide::ATTACKER) { if(vstd::contains(occupyableHexes, destHex+1)) return destHex+1; } else //if we are defender { if(vstd::contains(occupyableHexes, destHex-1)) return destHex-1; } break; } } return -1; } Rect CBattleInterface::hexPosition(BattleHex hex) const { int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX() + pos.x; int y = 86 + 42 *hex.getY() + pos.y; int w = cellShade->w; int h = cellShade->h; return Rect(x, y, w, h); } void CBattleInterface::obstaclePlaced(const CObstacleInstance & oi) { //so when multiple obstacles are added, they show up one after another waitForAnims(); //soundBase::soundID sound; // FIXME(v.markovtsev): soundh->playSound() is commented in the end => warning std::string defname; switch(oi.obstacleType) { case CObstacleInstance::SPELL_CREATED: { auto &spellObstacle = dynamic_cast(oi); defname = spellObstacle.appearAnimation; //TODO: sound //soundBase::QUIKSAND //soundBase::LANDMINE //soundBase::FORCEFLD //soundBase::fireWall } break; default: logGlobal->error("I don't know how to animate appearing obstacle of type %d", (int)oi.obstacleType); return; } auto animation = std::make_shared(defname); animation->preload(); auto first = animation->getImage(0, 0); if(!first) return; //we assume here that effect graphics have the same size as the usual obstacle image // -> if we know how to blit obstacle, let's blit the effect in the same place Point whereTo = getObstaclePosition(first, oi); addNewAnim(new CEffectAnimation(this, animation, whereTo.x, whereTo.y)); //TODO we need to wait after playing sound till it's finished, otherwise it overlaps and sounds really bad //CCS->soundh->playSound(sound); } void CBattleInterface::gateStateChanged(const EGateState state) { auto oldState = curInt->cb->battleGetGateState(); bool playSound = false; int stateId = EWallState::NONE; switch(state) { case EGateState::CLOSED: if (oldState != EGateState::BLOCKED) playSound = true; break; case EGateState::BLOCKED: if (oldState != EGateState::CLOSED) playSound = true; break; case EGateState::OPENED: playSound = true; stateId = EWallState::DAMAGED; break; case EGateState::DESTROYED: stateId = EWallState::DESTROYED; break; } if (oldState != EGateState::NONE && oldState != EGateState::CLOSED && oldState != EGateState::BLOCKED) SDL_FreeSurface(siegeH->walls[SiegeHelper::GATE]); if (stateId != EWallState::NONE) siegeH->walls[SiegeHelper::GATE] = BitmapHandler::loadBitmap(siegeH->getSiegeName(SiegeHelper::GATE, stateId)); if (playSound) CCS->soundh->playSound(soundBase::DRAWBRG); } const CGHeroInstance *CBattleInterface::currentHero() const { if (attackingHeroInstance->tempOwner == curInt->playerID) return attackingHeroInstance; else return defendingHeroInstance; } InfoAboutHero CBattleInterface::enemyHero() const { InfoAboutHero ret; if (attackingHeroInstance->tempOwner == curInt->playerID) curInt->cb->getHeroInfo(defendingHeroInstance, ret); else curInt->cb->getHeroInfo(attackingHeroInstance, ret); return ret; } void CBattleInterface::requestAutofightingAIToTakeAction() { assert(curInt->isAutoFightOn); boost::thread aiThread([&]() { auto ba = make_unique(curInt->autofightingAI->activeStack(activeStack)); if (curInt->isAutoFightOn) { if (tacticsMode) { // Always end tactics mode. Player interface is blocked currently, so it's not possible that // the AI can take any action except end tactics phase (AI actions won't be triggered) //TODO implement the possibility that the AI will be triggered for further actions //TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface? setActiveStack(nullptr); blockUI(true); tacticsMode = false; } else { givenCommand.setn(ba.release()); } } else { boost::unique_lock un(*CPlayerInterface::pim); activateStack(); } }); aiThread.detach(); } CBattleInterface::SiegeHelper::SiegeHelper(const CGTownInstance *siegeTown, const CBattleInterface *_owner) : owner(_owner), town(siegeTown) { for (int g = 0; g < ARRAY_COUNT(walls); ++g) { if (g != SiegeHelper::GATE) walls[g] = BitmapHandler::loadBitmap(getSiegeName(g)); } } CBattleInterface::SiegeHelper::~SiegeHelper() { auto gateState = owner->curInt->cb->battleGetGateState(); for (int g = 0; g < ARRAY_COUNT(walls); ++g) { if (g != SiegeHelper::GATE || (gateState != EGateState::NONE && gateState != EGateState::CLOSED && gateState != EGateState::BLOCKED)) SDL_FreeSurface(walls[g]); } } std::string CBattleInterface::SiegeHelper::getSiegeName(ui16 what) const { return getSiegeName(what, EWallState::INTACT); } std::string CBattleInterface::SiegeHelper::getSiegeName(ui16 what, int state) const { auto getImageIndex = [&]() -> int { switch (state) { case EWallState::INTACT : return 1; case EWallState::DAMAGED : if(what == 2 || what == 3 || what == 8) // towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2 return 1; else return 2; case EWallState::DESTROYED : if (what == 2 || what == 3 || what == 8) return 2; else return 3; } return 1; }; const std::string & prefix = town->town->clientInfo.siegePrefix; std::string addit = boost::lexical_cast(getImageIndex()); switch(what) { case SiegeHelper::BACKGROUND: return prefix + "BACK.BMP"; case SiegeHelper::BACKGROUND_WALL: { switch(town->town->faction->index) { case ETownType::RAMPART: case ETownType::NECROPOLIS: case ETownType::DUNGEON: case ETownType::STRONGHOLD: return prefix + "TPW1.BMP"; default: return prefix + "TPWL.BMP"; } } case SiegeHelper::KEEP: return prefix + "MAN" + addit + ".BMP"; case SiegeHelper::BOTTOM_TOWER: return prefix + "TW1" + addit + ".BMP"; case SiegeHelper::BOTTOM_WALL: return prefix + "WA1" + addit + ".BMP"; case SiegeHelper::WALL_BELLOW_GATE: return prefix + "WA3" + addit + ".BMP"; case SiegeHelper::WALL_OVER_GATE: return prefix + "WA4" + addit + ".BMP"; case SiegeHelper::UPPER_WALL: return prefix + "WA6" + addit + ".BMP"; case SiegeHelper::UPPER_TOWER: return prefix + "TW2" + addit + ".BMP"; case SiegeHelper::GATE: return prefix + "DRW" + addit + ".BMP"; case SiegeHelper::GATE_ARCH: return prefix + "ARCH.BMP"; case SiegeHelper::BOTTOM_STATIC_WALL: return prefix + "WA2.BMP"; case SiegeHelper::UPPER_STATIC_WALL: return prefix + "WA5.BMP"; case SiegeHelper::MOAT: return prefix + "MOAT.BMP"; case SiegeHelper::BACKGROUND_MOAT: return prefix + "MLIP.BMP"; case SiegeHelper::KEEP_BATTLEMENT: return prefix + "MANC.BMP"; case SiegeHelper::BOTTOM_BATTLEMENT: return prefix + "TW1C.BMP"; case SiegeHelper::UPPER_BATTLEMENT: return prefix + "TW2C.BMP"; default: return ""; } } void CBattleInterface::SiegeHelper::printPartOfWall(SDL_Surface *to, int what) { Point pos = Point(-1, -1); auto & ci = town->town->clientInfo; if (vstd::iswithin(what, 1, 17)) { pos.x = ci.siegePositions[what].x + owner->pos.x; pos.y = ci.siegePositions[what].y + owner->pos.y; } if (town->town->faction->index == ETownType::TOWER && (what == SiegeHelper::MOAT || what == SiegeHelper::BACKGROUND_MOAT)) return; // no moat in Tower. TODO: remove hardcode somehow? if (pos.x != -1) { //gate have no displayed bitmap when drawbridge is raised if (what == SiegeHelper::GATE) { auto gateState = owner->curInt->cb->battleGetGateState(); if (gateState != EGateState::OPENED && gateState != EGateState::DESTROYED) return; } blitAt(walls[what], pos.x, pos.y, to); } } CatapultProjectileInfo::CatapultProjectileInfo(Point from, Point dest) { facA = 0.005; // seems to be constant // system of 2 linear equations, solutions of which are missing coefficients // for quadratic equation a*x*x + b*x + c double eq[2][3] = { { static_cast(from.x), 1.0, from.y - facA*from.x*from.x }, { static_cast(dest.x), 1.0, dest.y - facA*dest.x*dest.x } }; // solve system via determinants double det = eq[0][0] *eq[1][1] - eq[1][0] *eq[0][1]; double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1]; double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2]; facB = detB / det; facC = detC / det; // make sure that parabola is correct e.g. passes through from and dest assert(fabs(calculateY(from.x) - from.y) < 1.0); assert(fabs(calculateY(dest.x) - dest.y) < 1.0); } double CatapultProjectileInfo::calculateY(double x) { return facA *pow(x, 2.0) + facB *x + facC; } void CBattleInterface::showAll(SDL_Surface *to) { show(to); } void CBattleInterface::show(SDL_Surface *to) { assert(to); SDL_Rect buf; SDL_GetClipRect(to, &buf); SDL_SetClipRect(to, &pos); ++animCount; showBackground(to); showBattlefieldObjects(to); showProjectiles(to); updateBattleAnimations(); SDL_SetClipRect(to, &buf); //restoring previous clip_rect showInterface(to); //activation of next stack if (pendingAnims.empty() && stackToActivate != nullptr) { activateStack(); //we may have changed active interface (another side in hot-seat), // so we can't continue drawing with old setting. show(to); } } void CBattleInterface::showBackground(SDL_Surface *to) { if (activeStack != nullptr && creAnims[activeStack->ID]->isIdle()) //show everything with range { // FIXME: any *real* reason to keep this separate? Speed difference can't be that big blitAt(backgroundWithHexes, pos.x, pos.y, to); } else { showBackgroundImage(to); showAbsoluteObstacles(to); } showHighlightedHexes(to); } void CBattleInterface::showBackgroundImage(SDL_Surface *to) { blitAt(background, pos.x, pos.y, to); if (settings["battle"]["cellBorders"].Bool()) { CSDL_Ext::blit8bppAlphaTo24bpp(cellBorders, nullptr, to, &pos); } } void CBattleInterface::showAbsoluteObstacles(SDL_Surface * to) { //Blit absolute obstacles for(auto & oi : curInt->cb->battleGetAllObstacles()) { if(oi->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { auto img = getObstacleImage(*oi); if(img) img->draw(to, pos.x + oi->getInfo().width, pos.y + oi->getInfo().height); } } if (siegeH && siegeH->town->hasBuilt(BuildingID::CITADEL)) siegeH->printPartOfWall(to, SiegeHelper::BACKGROUND_MOAT); } void CBattleInterface::showHighlightedHexes(SDL_Surface *to) { bool delayedBlit = false; //workaround for blitting enemy stack hex without mouse shadow with stack range on if(activeStack && settings["battle"]["stackRange"].Bool()) { std::set set = curInt->cb->battleGetAttackedHexes(activeStack, currentlyHoveredHex, attackingHex); for(BattleHex hex : set) if(hex != currentlyHoveredHex) showHighlightedHex(to, hex); // display the movement shadow of the stack at b (i.e. stack under mouse) const CStack * const shere = curInt->cb->battleGetStackByPos(currentlyHoveredHex, false); if(shere && shere != activeStack && shere->alive()) { std::vector v = curInt->cb->battleGetAvailableHexes(shere, true, nullptr); for(BattleHex hex : v) { if(hex != currentlyHoveredHex) showHighlightedHex(to, hex); else if(!settings["battle"]["mouseShadow"].Bool()) delayedBlit = true; //blit at the end of method to avoid graphic artifacts else showHighlightedHex(to, hex, true); //blit now and blit 2nd time later for darker shadow - avoids graphic artifacts } } } for(int b=0; bstrictHovered && bfield[b]->hovered) { if(previouslyHoveredHex == -1) previouslyHoveredHex = b; //something to start with if(currentlyHoveredHex == -1) currentlyHoveredHex = b; //something to start with if(currentlyHoveredHex != b) //repair hover info { previouslyHoveredHex = currentlyHoveredHex; currentlyHoveredHex = b; } if(settings["battle"]["mouseShadow"].Bool() || delayedBlit) { const spells::Caster *caster = nullptr; const CSpell *spell = nullptr; spells::Mode mode = spells::Mode::HERO; if(spellToCast)//hero casts spell { spell = SpellID(spellToCast->actionSubtype).toSpell(); caster = getActiveHero(); } else if(creatureSpellToCast >= 0 && stackCanCastSpell && creatureCasting)//stack casts spell { spell = SpellID(creatureSpellToCast).toSpell(); caster = activeStack; mode = spells::Mode::CREATURE_ACTIVE; } if(caster && spell) //when casting spell { // printing shaded hex(es) auto shaded = spell->rangeInHexes(curInt->cb.get(), mode, caster, currentlyHoveredHex); for(BattleHex shadedHex : shaded) { if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1)) showHighlightedHex(to, shadedHex, true); } } else if(active || delayedBlit) //always highlight pointed hex, keep this condition last in this method for correct behavior { if(currentlyHoveredHex.getX() != 0 && currentlyHoveredHex.getX() != GameConstants::BFIELD_WIDTH - 1) showHighlightedHex(to, currentlyHoveredHex, true); //keep true for OH3 behavior: hovered hex frame "thinner" } } } } } void CBattleInterface::showHighlightedHex(SDL_Surface *to, BattleHex hex, bool darkBorder) { int x = 14 + (hex.getY() % 2 == 0 ? 22 : 0) + 44 *(hex.getX()) + pos.x; int y = 86 + 42 *hex.getY() + pos.y; SDL_Rect temp_rect = genRect (cellShade->h, cellShade->w, x, y); CSDL_Ext::blit8bppAlphaTo24bpp (cellShade, nullptr, to, &temp_rect); if(!darkBorder && settings["battle"]["cellBorders"].Bool()) CSDL_Ext::blit8bppAlphaTo24bpp(cellBorder, nullptr, to, &temp_rect); //redraw border to make it light green instead of shaded } void CBattleInterface::showProjectiles(SDL_Surface *to) { assert(to); std::list< std::list::iterator > toBeDeleted; for (auto it = projectiles.begin(); it!=projectiles.end(); ++it) { // Check if projectile is already visible (shooter animation did the shot) if (!it->shotDone) { // frame we're waiting for is reached OR animation has already finished if (creAnims[it->stackID]->getCurrentFrame() >= it->animStartDelay || creAnims[it->stackID]->isShooting() == false) { //at this point projectile should become visible creAnims[it->stackID]->pause(); // pause animation it->shotDone = true; } else continue; // wait... } size_t group = it->reverse ? 1 : 0; auto image = idToProjectile[it->creID]->getImage(it->frameNum, group, true); if(image) { SDL_Rect dst; dst.h = image->height(); dst.w = image->width(); dst.x = it->x - dst.w / 2; dst.y = it->y - dst.h / 2; image->draw(to, &dst, nullptr); } // Update projectile ++it->step; if (it->step == it->lastStep) { toBeDeleted.insert(toBeDeleted.end(), it); } else { if (it->catapultInfo) { // Parabolic shot of the trajectory, as follows: f(x) = ax^2 + bx + c it->x += it->dx; it->y = it->catapultInfo->calculateY(it->x); ++(it->frameNum); it->frameNum %= idToProjectile[it->creID]->size(0); } else { // Normal projectile, just add the calculated "deltas" to the x and y positions. it->x += it->dx; it->y += it->dy; } } } for (auto & elem : toBeDeleted) { // resume animation creAnims[elem->stackID]->play(); projectiles.erase(elem); } } void CBattleInterface::showBattlefieldObjects(SDL_Surface *to) { auto showHexEntry = [&](BattleObjectsByHex::HexData & hex) { showPiecesOfWall(to, hex.walls); showObstacles(to, hex.obstacles); showAliveStacks(to, hex.alive); showBattleEffects(to, hex.effects); }; BattleObjectsByHex objects = sortObjectsByHex(); // dead stacks should be blit first showStacks(to, objects.beforeAll.dead); for (auto & data : objects.hex) showStacks(to, data.dead); showStacks(to, objects.afterAll.dead); // display objects that must be blit before anything else (e.g. topmost walls) showHexEntry(objects.beforeAll); // show heroes after "beforeAll" - e.g. topmost wall in siege if (attackingHero) attackingHero->show(to); if (defendingHero) defendingHero->show(to); // actual blit of most of objects, hex by hex // NOTE: row-by-row blitting may be a better approach for (auto &data : objects.hex) showHexEntry(data); // objects that must be blit *after* everything else - e.g. bottom tower or some spell effects showHexEntry(objects.afterAll); } void CBattleInterface::showAliveStacks(SDL_Surface *to, std::vector stacks) { BattleHex currentActionTarget; if(curInt->curAction) { auto target = curInt->curAction->getTarget(curInt->cb.get()); if(!target.empty()) currentActionTarget = target.at(0).hexValue; } auto isAmountBoxVisible = [&](const CStack *stack) -> bool { if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->getCount() == 1) //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature return false; if(stack->getCount() == 0) //hide box when target is going to die anyway - do not display "0 creatures" return false; for(auto anim : pendingAnims) //no matter what other conditions below are, hide box when creature is playing hit animation { auto hitAnimation = dynamic_cast(anim.first); if(hitAnimation && (hitAnimation->stack->ID == stack->ID)) //we process only "current creature" as other creatures will be processed reliably on their own iteration return false; } if(curInt->curAction) { if(curInt->curAction->stackNumber == stack->ID) //stack is currently taking action (is not a target of another creature's action etc) { if(curInt->curAction->actionType == EActionType::WALK || curInt->curAction->actionType == EActionType::SHOOT) //hide when stack walks or shoots return false; else if(curInt->curAction->actionType == EActionType::WALK_AND_ATTACK && currentActionTarget != stack->getPosition()) //when attacking, hide until walk phase finished return false; } if(curInt->curAction->actionType == EActionType::SHOOT && currentActionTarget == stack->getPosition()) //hide if we are ranged attack target return false; } return true; }; auto getEffectsPositivness = [&](const std::vector & activeSpells) -> int { int pos = 0; for (const auto & spellId : activeSpells) { pos += CGI->spellh->objects.at(spellId)->positiveness; } return pos; }; auto getAmountBoxBackground = [&](int positivness) -> SDL_Surface * { if (positivness > 0) return amountPositive; if (positivness < 0) return amountNegative; return amountEffNeutral; }; showStacks(to, stacks); // Actual display of all stacks for (auto & stack : stacks) { assert(stack); //printing amount if (isAmountBoxVisible(stack)) { const int sideShift = stack->side == BattleSide::ATTACKER ? 1 : -1; const int reverseSideShift = stack->side == BattleSide::ATTACKER ? -1 : 1; const BattleHex nextPos = stack->getPosition() + sideShift; const bool edge = stack->getPosition() % GameConstants::BFIELD_WIDTH == (stack->side == BattleSide::ATTACKER ? GameConstants::BFIELD_WIDTH - 2 : 1); const bool moveInside = !edge && !stackCountOutsideHexes[nextPos]; int xAdd = (stack->side == BattleSide::ATTACKER ? 220 : 202) + (stack->doubleWide() ? 44 : 0) * sideShift + (moveInside ? amountNormal->w + 10 : 0) * reverseSideShift; int yAdd = 260 + ((stack->side == BattleSide::ATTACKER || moveInside) ? 0 : -15); //blitting amount background box SDL_Surface *amountBG = amountNormal; std::vector activeSpells = stack->activeSpells(); if (!activeSpells.empty()) amountBG = getAmountBoxBackground(getEffectsPositivness(activeSpells)); SDL_Rect temp_rect = genRect(amountBG->h, amountBG->w, creAnims[stack->ID]->pos.x + xAdd, creAnims[stack->ID]->pos.y + yAdd); SDL_BlitSurface(amountBG, nullptr, to, &temp_rect); //blitting amount Point textPos(creAnims[stack->ID]->pos.x + xAdd + amountNormal->w/2, creAnims[stack->ID]->pos.y + yAdd + amountNormal->h/2); graphics->fonts[FONT_TINY]->renderTextCenter(to, makeNumberShort(stack->getCount()), Colors::WHITE, textPos); } } } void CBattleInterface::showStacks(SDL_Surface *to, std::vector stacks) { for (const CStack *stack : stacks) { creAnims[stack->ID]->nextFrame(to, creDir[stack->ID]); // do actual blit creAnims[stack->ID]->incrementFrame(float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000); } } void CBattleInterface::showObstacles(SDL_Surface * to, std::vector> & obstacles) { for(auto & obstacle : obstacles) { auto img = getObstacleImage(*obstacle); if(img) { Point p = getObstaclePosition(img, *obstacle); img->draw(to, p.x, p.y); } } } void CBattleInterface::showBattleEffects(SDL_Surface *to, const std::vector &battleEffects) { for (auto & elem : battleEffects) { int currentFrame = floor(elem->currentFrame); currentFrame %= elem->animation->size(); auto img = elem->animation->getImage(currentFrame); SDL_Rect temp_rect = genRect(img->height(), img->width(), elem->x, elem->y); img->draw(to, &temp_rect, nullptr); } } void CBattleInterface::showInterface(SDL_Surface *to) { blitAt(menu, pos.x, 556 + pos.y, to); if (tacticsMode) { btactNext->showAll(to); btactEnd->showAll(to); } else { console->showAll(to); bConsoleUp->showAll(to); bConsoleDown->showAll(to); } //showing buttons bOptions->showAll(to); bSurrender->showAll(to); bFlee->showAll(to); bAutofight->showAll(to); bSpell->showAll(to); bWait->showAll(to); bDefence->showAll(to); //showing in-game console LOCPLINT->cingconsole->show(to); Rect posWithQueue = Rect(pos.x, pos.y, 800, 600); if (settings["battle"]["showQueue"].Bool()) { if (!queue->embedded) { posWithQueue.y -= queue->pos.h; posWithQueue.h += queue->pos.h; } queue->showAll(to); } //printing border around interface if (screen->w != 800 || screen->h !=600) { CMessage::drawBorder(curInt->playerID,to,posWithQueue.w + 28, posWithQueue.h + 28, posWithQueue.x-14, posWithQueue.y-15); } } BattleObjectsByHex CBattleInterface::sortObjectsByHex() { auto getCurrentPosition = [&](const CStack *stack) -> BattleHex { for (auto & anim : pendingAnims) { // certainly ugly workaround but fixes quite annoying bug // stack position will be updated only *after* movement is finished // before this - stack is always at its initial position. Thus we need to find // its current position. Which can be found only in this class if (CMovementAnimation *move = dynamic_cast(anim.first)) { if (move->stack == stack) return move->nextHex; } } return stack->getPosition(); }; BattleObjectsByHex sorted; auto stacks = curInt->cb->battleGetStacksIf([](const CStack *s) { return !s->isTurret(); }); // Sort creatures for (auto & stack : stacks) { if (creAnims.find(stack->ID) == creAnims.end()) //e.g. for summoned but not yet handled stacks continue; if (stack->initialPosition < 0) // turret shooters are handled separately continue; //FIXME: hack to ignore ghost stacks if ((creAnims[stack->ID]->getType() == CCreatureAnim::DEAD || creAnims[stack->ID]->getType() == CCreatureAnim::HOLDING) && stack->isGhost()) ;//ignore else if (!creAnims[stack->ID]->isDead()) { if (!creAnims[stack->ID]->isMoving()) sorted.hex[stack->getPosition()].alive.push_back(stack); else { // flying creature - just blit them over everyone else if (stack->hasBonusOfType(Bonus::FLYING)) sorted.afterAll.alive.push_back(stack); else//try to find current location sorted.hex[getCurrentPosition(stack)].alive.push_back(stack); } } else sorted.hex[stack->getPosition()].dead.push_back(stack); } // Sort battle effects (spells) for (auto & battleEffect : battleEffects) { if (battleEffect.position.isValid()) sorted.hex[battleEffect.position].effects.push_back(&battleEffect); else sorted.afterAll.effects.push_back(&battleEffect); } // Sort obstacles { std::map> backgroundObstacles; for (auto &obstacle : curInt->cb->battleGetAllObstacles()) { if (obstacle->obstacleType != CObstacleInstance::ABSOLUTE_OBSTACLE && obstacle->obstacleType != CObstacleInstance::MOAT) { backgroundObstacles[obstacle->pos] = obstacle; } } for (auto &op : backgroundObstacles) { sorted.beforeAll.obstacles.push_back(op.second); } } // Sort wall parts if (siegeH) { sorted.beforeAll.walls.push_back(SiegeHelper::BACKGROUND_WALL); sorted.hex[135].walls.push_back(SiegeHelper::KEEP); sorted.afterAll.walls.push_back(SiegeHelper::BOTTOM_TOWER); sorted.hex[182].walls.push_back(SiegeHelper::BOTTOM_WALL); sorted.hex[130].walls.push_back(SiegeHelper::WALL_BELLOW_GATE); sorted.hex[78].walls.push_back(SiegeHelper::WALL_OVER_GATE); sorted.hex[12].walls.push_back(SiegeHelper::UPPER_WALL); sorted.beforeAll.walls.push_back(SiegeHelper::UPPER_TOWER); sorted.hex[94].walls.push_back(SiegeHelper::GATE); sorted.hex[112].walls.push_back(SiegeHelper::GATE_ARCH); sorted.hex[165].walls.push_back(SiegeHelper::BOTTOM_STATIC_WALL); sorted.hex[45].walls.push_back(SiegeHelper::UPPER_STATIC_WALL); if (siegeH && siegeH->town->hasBuilt(BuildingID::CITADEL)) { sorted.beforeAll.walls.push_back(SiegeHelper::MOAT); //sorted.beforeAll.walls.push_back(SiegeHelper::BACKGROUND_MOAT); // blit as absolute obstacle sorted.hex[135].walls.push_back(SiegeHelper::KEEP_BATTLEMENT); } if (siegeH && siegeH->town->hasBuilt(BuildingID::CASTLE)) { sorted.afterAll.walls.push_back(SiegeHelper::BOTTOM_BATTLEMENT); sorted.beforeAll.walls.push_back(SiegeHelper::UPPER_BATTLEMENT); } } return sorted; } void CBattleInterface::updateBattleAnimations() { //handle animations for (auto & elem : pendingAnims) { if (!elem.first) //this animation should be deleted continue; if (!elem.second) { elem.second = elem.first->init(); } if (elem.second && elem.first) elem.first->nextFrame(); } //delete anims int preSize = pendingAnims.size(); for (auto it = pendingAnims.begin(); it != pendingAnims.end(); ++it) { if (it->first == nullptr) { pendingAnims.erase(it); it = pendingAnims.begin(); break; } } if (preSize > 0 && pendingAnims.empty()) { //anims ended blockUI(activeStack == nullptr); animsAreDisplayed.setn(false); } } std::shared_ptr CBattleInterface::getObstacleImage(const CObstacleInstance & oi) { int frameIndex = (animCount+1) *25 / getAnimSpeed(); std::shared_ptr animation; if(oi.obstacleType == CObstacleInstance::USUAL || oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { animation = obstacleAnimations[oi.uniqueID]; } else if(oi.obstacleType == CObstacleInstance::SPELL_CREATED) { const SpellCreatedObstacle * spellObstacle = dynamic_cast(&oi); if(!spellObstacle) return std::shared_ptr(); std::string animationName = spellObstacle->animation; auto cacheIter = animationsCache.find(animationName); if(cacheIter == animationsCache.end()) { logAnim->trace("Creating obstacle animation %s", animationName); animation = std::make_shared(animationName); animation->preload(); animationsCache[animationName] = animation; } else { animation = cacheIter->second; } } if(animation) { frameIndex %= animation->size(0); return animation->getImage(frameIndex, 0); } return nullptr; } Point CBattleInterface::getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle) { int offset = obstacle.getAnimationYOffset(image->height()); Rect r = hexPosition(obstacle.pos); r.y += 42 - image->height() + offset; return r.topLeft(); } void CBattleInterface::redrawBackgroundWithHexes(const CStack *activeStack) { attackableHexes.clear(); if (activeStack) occupyableHexes = curInt->cb->battleGetAvailableHexes(activeStack, true, &attackableHexes); curInt->cb->battleGetStackCountOutsideHexes(stackCountOutsideHexes); //prepare background graphic with hexes and shaded hexes blitAt(background, 0, 0, backgroundWithHexes); //draw absolute obstacles (cliffs and so on) for(auto & oi : curInt->cb->battleGetAllObstacles()) { if(oi->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { auto img = getObstacleImage(*oi); if(img) img->draw(backgroundWithHexes, oi->getInfo().width, oi->getInfo().height); } } if (settings["battle"]["stackRange"].Bool()) { std::vector hexesToShade = occupyableHexes; hexesToShade.insert(hexesToShade.end(), attackableHexes.begin(), attackableHexes.end()); for (BattleHex hex : hexesToShade) { int i = hex.getY(); //row int j = hex.getX()-1; //column int x = 58 + (i%2==0 ? 22 : 0) + 44*j; int y = 86 + 42 *i; SDL_Rect temp_rect = genRect(cellShade->h, cellShade->w, x, y); CSDL_Ext::blit8bppAlphaTo24bpp(cellShade, nullptr, backgroundWithHexes, &temp_rect); } } if(settings["battle"]["cellBorders"].Bool()) CSDL_Ext::blit8bppAlphaTo24bpp(cellBorders, nullptr, backgroundWithHexes, nullptr); } void CBattleInterface::showPiecesOfWall(SDL_Surface *to, std::vector pieces) { if (!siegeH) return; for (auto piece : pieces) { if (piece < 15) // not a tower - just print siegeH->printPartOfWall(to, piece); else // tower. find if tower is built and not destroyed - stack is present { // PieceID StackID // 15 = keep, -2 // 16 = lower, -3 // 17 = upper, -4 // tower. check if tower is alive - stack is found int stackPos = 13 - piece; const CStack *turret = nullptr; for (auto & stack : curInt->cb->battleGetAllStacks(true)) { if(stack->initialPosition == stackPos) { turret = stack; break; } } if (turret) { std::vector stackList(1, turret); showStacks(to, stackList); siegeH->printPartOfWall(to, piece); } } } } vcmi-0.99+git20190113.f06c8a87/client/battle/CBattleInterface.h000066400000000000000000000416461342332007200232260ustar00rootroot00000000000000/* * CBattleInterface.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/ConstTransitivePtr.h" //may be reundant #include "../../lib/GameConstants.h" #include "CBattleAnimations.h" #include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation class CLabel; class CCreatureSet; class CGHeroInstance; class CStack; class CCallback; class CButton; class CToggleButton; class CToggleGroup; struct BattleResult; struct BattleSpellCast; struct CObstacleInstance; template struct CondSh; struct SetStackEffect; class BattleAction; class CGTownInstance; struct CatapultAttack; struct CatapultProjectileInfo; struct BattleTriggerEffect; class CBattleAnimation; class CBattleHero; class CBattleConsole; class CBattleResultWindow; class CStackQueue; class CPlayerInterface; class CCreatureAnimation; struct ProjectileInfo; class CClickableHex; struct BattleHex; struct InfoAboutHero; class CBattleGameInterface; struct CustomEffectInfo; class CAnimation; class IImage; /// Small struct which contains information about the id of the attacked stack, the damage dealt,... struct StackAttackedInfo { const CStack *defender; //attacked stack int64_t dmg; //damage dealt unsigned int amountKilled; //how many creatures in stack has been killed const CStack *attacker; //attacking stack bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack bool killed; //if true, stack has been killed bool rebirth; //if true, play rebirth animation after all bool cloneKilled; }; /// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,... struct BattleEffect { int x, y; //position on the screen float currentFrame; std::shared_ptr animation; int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim BattleHex position; //Indicates if effect which hex the effect is drawn on }; struct BattleObjectsByHex { typedef std::vector TWallList; typedef std::vector TStackList; typedef std::vector TEffectList; typedef std::vector> TObstacleList; struct HexData { TWallList walls; TStackList dead; TStackList alive; TEffectList effects; TObstacleList obstacles; }; HexData beforeAll; HexData afterAll; std::array hex; }; /// Small struct which is needed for drawing the parabolic trajectory of the catapult cannon struct CatapultProjectileInfo { CatapultProjectileInfo(Point from, Point dest); double facA, facB, facC; double calculateY(double x); }; /// Big class which handles the overall battle interface actions and it is also responsible for /// drawing everything correctly. class CBattleInterface : public WindowBase { enum PossibleActions // actions performed at l-click { INVALID = -1, CREATURE_INFO, MOVE_TACTICS, CHOOSE_TACTICS_STACK, MOVE_STACK, ATTACK, WALK_AND_ATTACK, ATTACK_AND_RETURN, SHOOT, //OPEN_GATE, //we can open castle gate during siege NO_LOCATION, ANY_LOCATION, OBSTACLE, TELEPORT, SACRIFICE, RANDOM_GENIE_SPELL, FREE_LOCATION, //used with Force Field and Fire Wall - all tiles affected by spell must be free CATAPULT, HEAL, RISE_DEMONS, AIMED_SPELL_CREATURE }; private: SDL_Surface *background, *menu, *amountNormal, *amountNegative, *amountPositive, *amountEffNeutral, *cellBorders, *backgroundWithHexes; std::shared_ptr bOptions; std::shared_ptr bSurrender; std::shared_ptr bFlee; std::shared_ptr bAutofight; std::shared_ptr bSpell; std::shared_ptr bWait; std::shared_ptr bDefence; std::shared_ptr bConsoleUp; std::shared_ptr bConsoleDown; std::shared_ptr btactNext; std::shared_ptr btactEnd; std::shared_ptr console; std::shared_ptr attackingHero; std::shared_ptr defendingHero; std::shared_ptr queue; const CCreatureSet *army1, *army2; //copy of initial armies (for result window) const CGHeroInstance *attackingHeroInstance, *defendingHeroInstance; std::map> creAnims; //animations of creatures from fighting armies (order by BattleInfo's stacks' ID) std::map> idToProjectile; std::map> animationsCache; std::map> obstacleAnimations; std::map creDir; // //TODO: move it to battle callback ui8 animCount; const CStack *activeStack; //number of active stack; nullptr - no one const CStack *mouseHoveredStack; // stack below mouse pointer, used for border animation const CStack *stackToActivate; //when animation is playing, we should wait till the end to make the next stack active; nullptr of none const CStack *selectedStack; //for Teleport / Sacrifice void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all std::vector occupyableHexes, //hexes available for active stack attackableHexes; //hexes attackable by active stack bool stackCountOutsideHexes[GameConstants::BFIELD_SIZE]; // hexes that when in front of a unit cause it's amount box to move back BattleHex previouslyHoveredHex; //number of hex that was hovered by the cursor a while ago BattleHex currentlyHoveredHex; //number of hex that is supposed to be hovered (for a while it may be inappropriately set, but will be renewed soon) int attackingHex; //hex from which the stack would perform attack with current cursor std::shared_ptr tacticianInterface; //used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players bool tacticsMode; bool stackCanCastSpell; //if true, active stack could possibly cast some target spell bool creatureCasting; //if true, stack currently aims to cats a spell bool spellDestSelectMode; //if true, player is choosing destination for his spell - only for GUI / console std::shared_ptr spellToCast; //spell for which player is choosing destination const CSpell *sp; //spell pointer for convenience si32 creatureSpellToCast; std::vector possibleActions; //all actions possible to call at the moment by player std::vector localActions; //actions possible to take on hovered hex std::vector illegalActions; //these actions display message in case of illegal target PossibleActions currentAction; //action that will be performed on l-click PossibleActions selectedAction; //last action chosen (and saved) by player PossibleActions illegalAction; //most likely action that can't be performed here void setActiveStack(const CStack *stack); void setHoveredStack(const CStack *stack); void requestAutofightingAIToTakeAction(); void getPossibleActionsForStack (const CStack *stack, const bool forceCast); //called when stack gets its turn void endCastingSpell(); //ends casting spell (eg. when spell has been cast or canceled) //force active stack to cast a spell if possible void enterCreatureCastingMode(); void printConsoleAttacked(const CStack *defender, int dmg, int killed, const CStack *attacker, bool Multiple); std::list projectiles; //projectiles flying on battlefield void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1); void sendCommand(BattleAction *& command, const CStack * actor = nullptr); bool isTileAttackable(const BattleHex & number) const; //returns true if tile 'number' is neighboring any tile from active stack's range or is one of these tiles bool isCatapultAttackable(BattleHex hex) const; //returns true if given tile can be attacked by catapult std::list battleEffects; //different animations to display on the screen like spell effects /// Class which is responsible for drawing the wall of a siege during battle class SiegeHelper { private: SDL_Surface* walls[18]; const CBattleInterface *owner; public: const CGTownInstance *town; //besieged town SiegeHelper(const CGTownInstance *siegeTown, const CBattleInterface *_owner); ~SiegeHelper(); std::string getSiegeName(ui16 what) const; std::string getSiegeName(ui16 what, int state) const; // state uses EWallState enum void printPartOfWall(SDL_Surface *to, int what); enum EWallVisual { BACKGROUND = 0, BACKGROUND_WALL = 1, KEEP, BOTTOM_TOWER, BOTTOM_WALL, WALL_BELLOW_GATE, WALL_OVER_GATE, UPPER_WALL, UPPER_TOWER, GATE, GATE_ARCH, BOTTOM_STATIC_WALL, UPPER_STATIC_WALL, MOAT, BACKGROUND_MOAT, KEEP_BATTLEMENT, BOTTOM_BATTLEMENT, UPPER_BATTLEMENT }; friend class CBattleInterface; } *siegeH; std::shared_ptr attackerInt, defenderInt; //because LOCPLINT is not enough in hotSeat std::shared_ptr curInt; //current player interface const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell /** Methods for displaying battle screen */ void showBackground(SDL_Surface *to); void showBackgroundImage(SDL_Surface *to); void showAbsoluteObstacles(SDL_Surface *to); void showHighlightedHexes(SDL_Surface *to); void showHighlightedHex(SDL_Surface *to, BattleHex hex, bool darkBorder = false); void showInterface(SDL_Surface *to); void showBattlefieldObjects(SDL_Surface *to); void showAliveStacks(SDL_Surface *to, std::vector stacks); void showStacks(SDL_Surface *to, std::vector stacks); void showObstacles(SDL_Surface *to, std::vector> &obstacles); void showPiecesOfWall(SDL_Surface *to, std::vector pieces); void showBattleEffects(SDL_Surface *to, const std::vector &battleEffects); void showProjectiles(SDL_Surface *to); BattleObjectsByHex sortObjectsByHex(); void updateBattleAnimations(); std::shared_ptr getObstacleImage(const CObstacleInstance & oi); Point getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle); void redrawBackgroundWithHexes(const CStack *activeStack); /** End of battle screen blitting methods */ PossibleActions getCasterAction(const CSpell *spell, const spells::Caster *caster, spells::Mode mode) const; void setHeroAnimation(ui8 side, int phase); public: static CondSh animsAreDisplayed; //for waiting with the end of battle for end of anims static CondSh givenCommand; //data != nullptr if we have i.e. moved current unit std::list> pendingAnims; //currently displayed animations void addNewAnim(CBattleAnimation *anim); //adds new anim to pendingAnims ui32 animIDhelper; //for giving IDs for animations CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); virtual ~CBattleInterface(); //std::vector timeinterested; //animation handling void setPrintCellBorders(bool set); //if true, cell borders will be printed void setPrintStackRange(bool set); //if true,range of active stack will be printed void setPrintMouseShadow(bool set); //if true, hex under mouse will be shaded void setAnimSpeed(int set); //speed of animation; range 1..100 int getAnimSpeed() const; //speed of animation; range 1..100 CPlayerInterface *getCurrentPlayerInterface() const; std::vector> bfield; //11 lines, 17 hexes on each SDL_Surface *cellBorder, *cellShade; bool myTurn; //if true, interface is active (commands can be ordered) bool moveStarted; //if true, the creature that is already moving is going to make its first step int moveSoundHander; // sound handler used when moving a unit const BattleResult *bresult; //result of a battle; if non-zero then display when all animations end // block all UI elements, e.g. during enemy turn // unlike activate/deactivate this method will correctly grey-out all elements void blockUI(bool on); //button handle funcs: void bOptionsf(); void bSurrenderf(); void bFleef(); void reallyFlee(); //performs fleeing without asking player void reallySurrender(); //performs surrendering without asking player void bAutofightf(); void bSpellf(); void bWaitf(); void bDefencef(); void bConsoleUpf(); void bConsoleDownf(); void bTacticNextStack(const CStack *current = nullptr); void bEndTacticPhase(); //end of button handle funcs //napisz tu klase odpowiadajaca za wyswietlanie bitwy i obsluge uzytkownika, polecenia ma przekazywac callbackiem void activate() override; void deactivate() override; void keyPressed(const SDL_KeyboardEvent & key) override; void mouseMoved(const SDL_MouseMotionEvent &sEvent) override; void clickRight(tribool down, bool previousState) override; void show(SDL_Surface *to) override; void showAll(SDL_Surface *to) override; //call-ins void startAction(const BattleAction* action); void unitAdded(const CStack * stack); //new stack appeared on battlefield void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled void stackActivated(const CStack *stack); //active stack has been changed void stackMoved(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex void waitForAnims(); void stacksAreAttacked(std::vector attackedInfos, const std::vector & battleLog); //called when a certain amount of stacks has been attacked void stackAttacking(const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting); //called when stack with id ID is attacking something on hex dest void newRoundFirst( int round ); void newRound(int number); //caled when round is ended; number is the number of round void hexLclicked(int whichOne); //hex only call-in void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls void battleFinished(const BattleResult& br); //called when battle is finished - battleresult window should be printed void displayBattleFinished(); //displays battle result void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook void displayBattleLog(const std::vector & battleLog); void displayCustomEffects(const std::vector & customEffects); void displayEffect(ui32 effect, BattleHex destTile); //displays custom effect on the battlefield void displaySpellCast(SpellID spellID, BattleHex destinationTile); //displays spell`s cast animation void displaySpellEffect(SpellID spellID, BattleHex destinationTile); //displays spell`s affected animation void displaySpellHit(SpellID spellID, BattleHex destinationTile); //displays spell`s affected animation void displaySpellAnimation(const CSpell::TAnimation & animation, BattleHex destinationTile); void battleTriggerEffect(const BattleTriggerEffect & bte); void setBattleCursor(const int myNumber); //really complex and messy, sets attackingHex void endAction(const BattleAction* action); void hideQueue(); void showQueue(); Rect hexPosition(BattleHex hex) const; void handleHex(BattleHex myNumber, int eventType); bool isCastingPossibleHere (const CStack *sactive, const CStack *shere, BattleHex myNumber); bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber); //TODO: move to BattleState / callback BattleHex fromWhichHexAttack(BattleHex myNumber); void obstaclePlaced(const CObstacleInstance & oi); void gateStateChanged(const EGateState state); void initStackProjectile(const CStack * stack); const CGHeroInstance *currentHero() const; InfoAboutHero enemyHero() const; friend class CPlayerInterface; friend class CButton; friend class CInGameConsole; friend class CBattleResultWindow; friend class CBattleHero; friend class CEffectAnimation; friend class CBattleStackAnimation; friend class CReverseAnimation; friend class CDefenceAnimation; friend class CMovementAnimation; friend class CMovementStartAnimation; friend class CAttackAnimation; friend class CMeleeAttackAnimation; friend class CShootingAnimation; friend class CCastAnimation; friend class CClickableHex; }; vcmi-0.99+git20190113.f06c8a87/client/battle/CBattleInterfaceClasses.cpp000066400000000000000000000651571342332007200251020ustar00rootroot00000000000000/* * CBattleInterfaceClasses.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CBattleInterfaceClasses.h" #include "CBattleInterface.h" #include "../CBitmapHandler.h" #include "../CGameInfo.h" #include "../CMessage.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../CVideoHandler.h" #include "../Graphics.h" #include "../gui/CAnimation.h" #include "../gui/CCursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/SDL_Extensions.h" #include "../widgets/Buttons.h" #include "../widgets/TextControls.h" #include "../windows/CCreatureWindow.h" #include "../windows/CSpellWindow.h" #include "../../CCallback.h" #include "../../lib/CStack.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CGameState.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CTownHandler.h" #include "../../lib/NetPacks.h" #include "../../lib/StartInfo.h" #include "../../lib/CondSh.h" #include "../../lib/mapObjects/CGTownInstance.h" void CBattleConsole::showAll(SDL_Surface * to) { Point textPos(pos.x + pos.w/2, pos.y + 17); if(ingcAlter.size()) { graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(ingcAlter, pos.w, FONT_SMALL), Colors::WHITE, textPos); } else if(alterTxt.size()) { graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(alterTxt, pos.w, FONT_SMALL), Colors::WHITE, textPos); } else if(texts.size()) { if(texts.size()==1) { graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(texts[0], pos.w, FONT_SMALL), Colors::WHITE, textPos); } else { graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(texts[lastShown - 1], pos.w, FONT_SMALL), Colors::WHITE, textPos); textPos.y += 16; graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(texts[lastShown], pos.w, FONT_SMALL), Colors::WHITE, textPos); } } } bool CBattleConsole::addText(const std::string & text) { logGlobal->trace("CBattleConsole message: %s", text); if(text.size()>70) return false; //text too long! int firstInToken = 0; for(size_t i = 0; i < text.size(); ++i) //tokenize { if(text[i] == 10) { texts.push_back( text.substr(firstInToken, i-firstInToken) ); firstInToken = i+1; } } texts.push_back( text.substr(firstInToken, text.size()) ); lastShown = texts.size()-1; return true; } void CBattleConsole::alterText(const std::string &text) { //char buf[500]; //sprintf(buf, text.c_str()); //alterTxt = buf; alterTxt = text; } void CBattleConsole::eraseText(ui32 pos) { if(pos < texts.size()) { texts.erase(texts.begin() + pos); if(lastShown == texts.size()) --lastShown; } } void CBattleConsole::changeTextAt(const std::string & text, ui32 pos) { if(pos >= texts.size()) //no such pos return; texts[pos] = text; } void CBattleConsole::scrollUp(ui32 by) { if(lastShown > static_cast(by)) lastShown -= by; } void CBattleConsole::scrollDown(ui32 by) { if(lastShown + by < texts.size()) lastShown += by; } CBattleConsole::CBattleConsole() : lastShown(-1), alterTxt(""), whoSetAlter(0) {} void CBattleHero::show(SDL_Surface * to) { auto flagFrame = flagAnimation->getImage(flagAnim, 0, true); if(!flagFrame) return; //animation of flag SDL_Rect temp_rect; if(flip) { temp_rect = genRect( flagFrame->height(), flagFrame->width(), pos.x + 61, pos.y + 39); } else { temp_rect = genRect( flagFrame->height(), flagFrame->width(), pos.x + 72, pos.y + 39); } flagFrame->draw(screen, &temp_rect, nullptr); //FIXME: why screen? //animation of hero SDL_Rect rect = pos; auto heroFrame = animation->getImage(currentFrame, phase, true); if(!heroFrame) return; heroFrame->draw(to, &rect, nullptr); if(++animCount >= 4) { animCount = 0; if(++flagAnim >= flagAnimation->size(0)) flagAnim = 0; if(++currentFrame >= lastFrame) switchToNextPhase(); } } void CBattleHero::setPhase(int newPhase) { nextPhase = newPhase; switchToNextPhase(); //immediately switch to next phase and then restore idling phase nextPhase = 0; } void CBattleHero::hover(bool on) { //TODO: Make lines below work properly if (on) CCS->curh->changeGraphic(ECursor::COMBAT, 5); else CCS->curh->changeGraphic(ECursor::COMBAT, 0); } void CBattleHero::clickLeft(tribool down, bool previousState) { if(myOwner->spellDestSelectMode) //we are casting a spell return; if(boost::logic::indeterminate(down)) return; if(!myHero || down || !myOwner->myTurn) return; if(myOwner->getCurrentPlayerInterface()->cb->battleCanCastSpell(myHero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions { for(int it=0; itbfield[it]->hovered && myOwner->bfield[it]->strictHovered) return; } CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); GH.pushIntT(myHero, myOwner->getCurrentPlayerInterface()); } } void CBattleHero::clickRight(tribool down, bool previousState) { if(boost::logic::indeterminate(down)) return; Point windowPosition; windowPosition.x = (!flip) ? myOwner->pos.topLeft().x + 1 : myOwner->pos.topRight().x - 79; windowPosition.y = myOwner->pos.y + 135; InfoAboutHero targetHero; if(down && (myOwner->myTurn || settings["session"]["spectate"].Bool())) { auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance; targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE); GH.pushIntT(targetHero, &windowPosition); } } void CBattleHero::switchToNextPhase() { if(phase != nextPhase) { phase = nextPhase; firstFrame = 0; lastFrame = animation->size(phase); } currentFrame = firstFrame; } CBattleHero::CBattleHero(const std::string & animationPath, bool flipG, PlayerColor player, const CGHeroInstance * hero, const CBattleInterface * owner): flip(flipG), myHero(hero), myOwner(owner), phase(1), nextPhase(0), flagAnim(0), animCount(0) { animation = std::make_shared(animationPath); animation->preload(); if(flipG) animation->verticalFlip(); if(flip) flagAnimation = std::make_shared("CMFLAGR"); else flagAnimation = std::make_shared("CMFLAGL"); flagAnimation->preload(); flagAnimation->playerColored(player); addUsedEvents(LCLICK | RCLICK | HOVER); switchToNextPhase(); } CBattleHero::~CBattleHero() = default; CHeroInfoWindow::CHeroInfoWindow(const InfoAboutHero & hero, Point * position) : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, "CHRPOP") { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); if (position != nullptr) moveTo(*position); background->colorize(hero.owner); //maybe add this functionality to base class? auto attack = hero.details->primskills[0]; auto defense = hero.details->primskills[1]; auto power = hero.details->primskills[2]; auto knowledge = hero.details->primskills[3]; auto morale = hero.details->morale; auto luck = hero.details->luck; auto currentSpellPoints = hero.details->mana; auto maxSpellPoints = hero.details->manaLimit; icons.push_back(std::make_shared("PortraitsLarge", hero.portrait, 0, 10, 6)); //primary stats labels.push_back(std::make_shared(9, 75, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":")); labels.push_back(std::make_shared(9, 87, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":")); labels.push_back(std::make_shared(9, 99, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[382] + ":")); labels.push_back(std::make_shared(9, 111, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[383] + ":")); labels.push_back(std::make_shared(69, 87, EFonts::FONT_TINY, EAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack))); labels.push_back(std::make_shared(69, 99, EFonts::FONT_TINY, EAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense))); labels.push_back(std::make_shared(69, 111, EFonts::FONT_TINY, EAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power))); labels.push_back(std::make_shared(69, 123, EFonts::FONT_TINY, EAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge))); //morale+luck labels.push_back(std::make_shared(9, 131, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":")); labels.push_back(std::make_shared(9, 143, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":")); icons.push_back(std::make_shared("IMRL22", morale + 3, 0, 47, 131)); icons.push_back(std::make_shared("ILCK22", luck + 3, 0, 47, 143)); //spell points labels.push_back(std::make_shared(39, 174, EFonts::FONT_TINY, EAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387])); labels.push_back(std::make_shared(39, 186, EFonts::FONT_TINY, EAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints))); } CBattleOptionsWindow::CBattleOptionsWindow(const SDL_Rect & position, CBattleInterface *owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); pos = position; background = std::make_shared("comopbck.bmp"); background->colorize(owner->getCurrentPlayerInterface()->playerID); auto viewGrid = std::make_shared(Point(25, 56), "sysopchk.def", CGI->generaltexth->zelp[427], [=](bool on){owner->setPrintCellBorders(on);} ); viewGrid->setSelected(settings["battle"]["cellBorders"].Bool()); toggles.push_back(viewGrid); auto movementShadow = std::make_shared(Point(25, 89), "sysopchk.def", CGI->generaltexth->zelp[428], [=](bool on){owner->setPrintStackRange(on);}); movementShadow->setSelected(settings["battle"]["stackRange"].Bool()); toggles.push_back(movementShadow); auto mouseShadow = std::make_shared(Point(25, 122), "sysopchk.def", CGI->generaltexth->zelp[429], [=](bool on){owner->setPrintMouseShadow(on);}); mouseShadow->setSelected(settings["battle"]["mouseShadow"].Bool()); toggles.push_back(mouseShadow); animSpeeds = std::make_shared([=](int value){ owner->setAnimSpeed(value);}); std::shared_ptr toggle; toggle = std::make_shared(Point( 28, 225), "sysopb9.def", CGI->generaltexth->zelp[422]); animSpeeds->addToggle(40, toggle); toggle = std::make_shared(Point( 92, 225), "sysob10.def", CGI->generaltexth->zelp[423]); animSpeeds->addToggle(63, toggle); toggle = std::make_shared(Point(156, 225), "sysob11.def", CGI->generaltexth->zelp[424]); animSpeeds->addToggle(100, toggle); animSpeeds->setSelected(owner->getAnimSpeed()); setToDefault = std::make_shared(Point(246, 359), "codefaul.def", CGI->generaltexth->zelp[393], [&](){ bDefaultf(); }); setToDefault->setImageOrder(1, 0, 2, 3); exit = std::make_shared(Point(357, 359), "soretrn.def", CGI->generaltexth->zelp[392], [&](){ bExitf();}, SDLK_RETURN); exit->setImageOrder(1, 0, 2, 3); //creating labels labels.push_back(std::make_shared(242, 32, FONT_BIG, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[392]));//window title labels.push_back(std::make_shared(122, 214, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[393]));//animation speed labels.push_back(std::make_shared(122, 293, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[394]));//music volume labels.push_back(std::make_shared(122, 359, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[395]));//effects' volume labels.push_back(std::make_shared(353, 66, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[396]));//auto - combat options labels.push_back(std::make_shared(353, 265, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[397]));//creature info //auto - combat options labels.push_back(std::make_shared(283, 86, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[398]));//creatures labels.push_back(std::make_shared(283, 116, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[399]));//spells labels.push_back(std::make_shared(283, 146, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[400]));//catapult labels.push_back(std::make_shared(283, 176, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[151]));//ballista labels.push_back(std::make_shared(283, 206, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[401]));//first aid tent //creature info labels.push_back(std::make_shared(283, 285, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[402]));//all stats labels.push_back(std::make_shared(283, 315, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[403]));//spells only //general options labels.push_back(std::make_shared(61, 57, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[404])); labels.push_back(std::make_shared(61, 90, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[405])); labels.push_back(std::make_shared(61, 123, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[406])); labels.push_back(std::make_shared(61, 156, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[407])); } void CBattleOptionsWindow::bDefaultf() { //TODO: implement } void CBattleOptionsWindow::bExitf() { close(); } CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterface & _owner) : owner(_owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); pos = genRect(561, 470, (screen->w - 800)/2 + 165, (screen->h - 600)/2 + 19); background = std::make_shared("CPRESULT"); background->colorize(owner.playerID); exit = std::make_shared(Point(384, 505), "iok6432.def", std::make_pair("", ""), [&](){ bExitf();}, SDLK_RETURN); exit->setBorderColor(Colors::METALLIC_GOLD); if(br.winner==0) //attacker won { labels.push_back(std::make_shared(59, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); labels.push_back(std::make_shared(408, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); } else //if(br.winner==1) { labels.push_back(std::make_shared(59, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); labels.push_back(std::make_shared(412, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); } labels.push_back(std::make_shared(232, 302, FONT_BIG, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[407])); labels.push_back(std::make_shared(232, 332, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408])); labels.push_back(std::make_shared(232, 428, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[409])); std::string sideNames[2] = {"N/A", "N/A"}; for(int i = 0; i < 2; i++) { auto heroInfo = owner.cb->battleGetHeroInfo(i); const int xs[] = {21, 392}; if(heroInfo.portrait >= 0) //attacking hero { icons.push_back(std::make_shared("PortraitsLarge", heroInfo.portrait, 0, xs[i], 38)); sideNames[i] = heroInfo.name; } else { auto stacks = owner.cb->battleGetAllStacks(); vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison { return stack->side != i || !stack->base; }); auto best = vstd::maxElementByFun(stacks, [](const CStack * stack) { return stack->type->AIValue; }); if(best != stacks.end()) //should be always but to be safe... { icons.push_back(std::make_shared("TWCRPORT", (*best)->type->idNumber+2, 0, xs[i], 38)); sideNames[i] = CGI->creh->creatures[(*best)->type->idNumber]->namePl; } } } //printing attacker and defender's names labels.push_back(std::make_shared(89, 37, FONT_SMALL, TOPLEFT, Colors::WHITE, sideNames[0])); labels.push_back(std::make_shared(381, 53, FONT_SMALL, BOTTOMRIGHT, Colors::WHITE, sideNames[1])); //printing casualties for(int step = 0; step < 2; ++step) { if(br.casualties[step].size()==0) { labels.push_back(std::make_shared(235, 360 + 97 * step, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[523])); } else { int xPos = 235 - (br.casualties[step].size()*32 + (br.casualties[step].size() - 1)*10)/2; //increment by 42 with each picture int yPos = 344 + step * 97; for(auto & elem : br.casualties[step]) { icons.push_back(std::make_shared("CPRSMALL", CGI->creh->creatures[elem.first]->iconIndex, 0, xPos, yPos)); std::ostringstream amount; amount<(xPos + 16, yPos + 42, FONT_SMALL, CENTER, Colors::WHITE, amount.str())); xPos += 42; } } } //printing result description bool weAreAttacker = !(owner.cb->battleGetMySide()); if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won { int text = 304; switch(br.result) { case BattleResult::NORMAL: break; case BattleResult::ESCAPE: text = 303; break; case BattleResult::SURRENDER: text = 302; break; default: logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); break; } CCS->musich->playMusic("Music/Win Battle", false); CCS->videoh->open("WIN3.BIK"); std::string str = CGI->generaltexth->allTexts[text]; const CGHeroInstance * ourHero = owner.cb->battleGetMyHero(); if (ourHero) { str += CGI->generaltexth->allTexts[305]; boost::algorithm::replace_first(str, "%s", ourHero->name); boost::algorithm::replace_first(str, "%d", boost::lexical_cast(br.exp[weAreAttacker ? 0 : 1])); } description = std::make_shared(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, CENTER, Colors::WHITE); } else // we lose { int text = 311; std::string musicName = "Music/LoseCombat"; std::string videoName = "LBSTART.BIK"; switch(br.result) { case BattleResult::NORMAL: break; case BattleResult::ESCAPE: musicName = "Music/Retreat Battle"; videoName = "RTSTART.BIK"; text = 310; break; case BattleResult::SURRENDER: musicName = "Music/Surrender Battle"; videoName = "SURRENDER.BIK"; text = 309; break; default: logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); break; } CCS->musich->playMusic(musicName, false); CCS->videoh->open(videoName); labels.push_back(std::make_shared(235, 235, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text])); } } CBattleResultWindow::~CBattleResultWindow() = default; void CBattleResultWindow::activate() { owner.showingDialog->set(true); CIntObject::activate(); } void CBattleResultWindow::show(SDL_Surface * to) { CIntObject::show(to); CCS->videoh->update(pos.x + 107, pos.y + 70, screen, true, false); } void CBattleResultWindow::bExitf() { CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon close(); if(dynamic_cast(GH.topInt().get())) GH.popInts(1); //pop battle interface if present //Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle, //so we can be sure that there is no dialogs left on GUI stack. intTmp.showingDialog->setn(false); CCS->videoh->close(); } Point CClickableHex::getXYUnitAnim(BattleHex hexNum, const CStack * stack, CBattleInterface * cbi) { assert(cbi); Point ret(-500, -500); //returned value if(stack && stack->initialPosition < 0) //creatures in turrets { switch(stack->initialPosition) { case -2: //keep ret = cbi->siegeH->town->town->clientInfo.siegePositions[18]; break; case -3: //lower turret ret = cbi->siegeH->town->town->clientInfo.siegePositions[19]; break; case -4: //upper turret ret = cbi->siegeH->town->town->clientInfo.siegePositions[20]; break; } } else { static const Point basePos(-190, -139); // position of creature in topleft corner static const int imageShiftX = 30; // X offset to base pos for facing right stacks, negative for facing left ret.x = basePos.x + 22 * ( (hexNum.getY() + 1)%2 ) + 44 * hexNum.getX(); ret.y = basePos.y + 42 * hexNum.getY(); if (stack) { if(cbi->creDir[stack->ID]) ret.x += imageShiftX; else ret.x -= imageShiftX; //shifting position for double - hex creatures if(stack->doubleWide()) { if(stack->side == BattleSide::ATTACKER) { if(cbi->creDir[stack->ID]) ret.x -= 44; } else { if(!cbi->creDir[stack->ID]) ret.x += 44; } } } } //returning return ret + CPlayerInterface::battleInt->pos; } void CClickableHex::hover(bool on) { hovered = on; //Hoverable::hover(on); if(!on && setAlterText) { myInterface->console->alterTxt = std::string(); setAlterText = false; } } CClickableHex::CClickableHex() : setAlterText(false), myNumber(-1), accessible(true), strictHovered(false), myInterface(nullptr) { addUsedEvents(LCLICK | RCLICK | HOVER | MOVE); } void CClickableHex::mouseMoved(const SDL_MouseMotionEvent &sEvent) { if(myInterface->cellShade) { if(CSDL_Ext::SDL_GetPixel(myInterface->cellShade, sEvent.x-pos.x, sEvent.y-pos.y) == 0) //hovered pixel is outside hex { strictHovered = false; } else //hovered pixel is inside hex { strictHovered = true; } } if(hovered && strictHovered) //print attacked creature to console { const CStack * attackedStack = myInterface->getCurrentPlayerInterface()->cb->battleGetStackByPos(myNumber); if(myInterface->console->alterTxt.size() == 0 &&attackedStack != nullptr && attackedStack->owner != myInterface->getCurrentPlayerInterface()->playerID && attackedStack->alive()) { MetaString text; text.addTxt(MetaString::GENERAL_TXT, 220); attackedStack->addNameReplacement(text); myInterface->console->alterTxt = text.toString(); setAlterText = true; } } else if(setAlterText) { myInterface->console->alterTxt = std::string(); setAlterText = false; } } void CClickableHex::clickLeft(tribool down, bool previousState) { if(!down && hovered && strictHovered) //we've been really clicked! { myInterface->hexLclicked(myNumber); } } void CClickableHex::clickRight(tribool down, bool previousState) { const CStack * myst = myInterface->getCurrentPlayerInterface()->cb->battleGetStackByPos(myNumber); //stack info if(hovered && strictHovered && myst!=nullptr) { if(!myst->alive()) return; if(down) { GH.pushIntT(myst, true); } } } CStackQueue::CStackQueue(bool Embedded, CBattleInterface * _owner) : embedded(Embedded), owner(_owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); if(embedded) { pos.w = QUEUE_SIZE * 37; pos.h = 46; pos.x = screen->w/2 - pos.w/2; pos.y = (screen->h - 600)/2 + 10; icons = std::make_shared("CPRSMALL"); stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESSMALL"); } else { pos.w = 800; pos.h = 85; background = std::make_shared("DIBOXBCK", Rect(0, 0, pos.w, pos.h)); icons = std::make_shared("TWCRPORT"); stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESSMALL"); //TODO: where use big icons? //stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESBIG"); } stateIcons->preload(); stackBoxes.resize(QUEUE_SIZE); for (int i = 0; i < stackBoxes.size(); i++) { stackBoxes[i] = std::make_shared(this); stackBoxes[i]->moveBy(Point(1 + (embedded ? 36 : 80) * i, 0)); } } CStackQueue::~CStackQueue() = default; void CStackQueue::update() { std::vector queueData; owner->getCurrentPlayerInterface()->cb->battleGetTurnOrder(queueData, stackBoxes.size(), 0); size_t boxIndex = 0; for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++) { for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++) stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn); } for(; boxIndex < stackBoxes.size(); boxIndex++) stackBoxes[boxIndex]->setUnit(nullptr); } CStackQueue::StackBox::StackBox(CStackQueue * owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); background = std::make_shared(owner->embedded ? "StackQueueSmall" : "StackQueueLarge"); pos.w = background->pos.w; pos.h = background->pos.h; if(owner->embedded) { icon = std::make_shared(owner->icons, 0, 0, 5, 2); amount = std::make_shared(pos.w/2, pos.h - 7, FONT_SMALL, CENTER, Colors::WHITE); } else { icon = std::make_shared(owner->icons, 0, 0, 9, 1); amount = std::make_shared(pos.w/2, pos.h - 8, FONT_MEDIUM, CENTER, Colors::WHITE); int icon_x = pos.w - 17; int icon_y = pos.h - 18; stateIcon = std::make_shared(owner->stateIcons, 0, 0, icon_x, icon_y); stateIcon->visible = false; } } void CStackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn) { if(unit) { background->colorize(unit->unitOwner()); icon->visible = true; icon->setFrame(unit->creatureIconIndex()); amount->setText(makeNumberShort(unit->getCount())); if(stateIcon) { if(unit->defended(turn) || (turn > 0 && unit->defended(turn - 1))) { stateIcon->setFrame(0, 0); stateIcon->visible = true; } else if(unit->waited(turn)) { stateIcon->setFrame(1, 0); stateIcon->visible = true; } else { stateIcon->visible = false; } } } else { background->colorize(PlayerColor::NEUTRAL); icon->visible = false; icon->setFrame(0); amount->setText(""); if(stateIcon) stateIcon->visible = false; } } vcmi-0.99+git20190113.f06c8a87/client/battle/CBattleInterfaceClasses.h000066400000000000000000000136671342332007200245460ustar00rootroot00000000000000/* * CBattleInterfaceClasses.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../gui/CIntObject.h" #include "../../lib/battle/BattleHex.h" #include "../windows/CWindowObject.h" struct SDL_Surface; class CGHeroInstance; class CBattleInterface; class CPicture; class CFilledTexture; class CButton; class CToggleButton; class CToggleGroup; class CLabel; class CTextBox; struct BattleResult; class CStack; namespace battle { class Unit; } class CAnimImage; class CPlayerInterface; /// Class which shows the console at the bottom of the battle screen and manages the text of the console class CBattleConsole : public CIntObject { private: std::vector< std::string > texts; //a place where texts are stored int lastShown; //last shown line of text public: std::string alterTxt; //if it's not empty, this text is displayed std::string ingcAlter; //alternative text set by in-game console - very important! int whoSetAlter; //who set alter text; 0 - battle interface or none, 1 - button CBattleConsole(); void showAll(SDL_Surface * to = 0) override; bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters) void alterText(const std::string &text); //place string at alterTxt void eraseText(ui32 pos); //erases added text at position pos void changeTextAt(const std::string &text, ui32 pos); //if we have more than pos texts, pos-th is changed to given one void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions }; /// Hero battle animation class CBattleHero : public CIntObject { void switchToNextPhase(); public: bool flip; //false if it's attacking hero, true otherwise std::shared_ptr animation; std::shared_ptr flagAnimation; const CGHeroInstance * myHero; //this animation's hero instance const CBattleInterface * myOwner; //battle interface to which this animation is assigned int phase; //stage of animation int nextPhase; //stage of animation to be set after current phase is fully displayed int currentFrame, firstFrame, lastFrame; //frame of animation size_t flagAnim; ui8 animCount; //for flag animation void show(SDL_Surface * to) override; //prints next frame of animation to to void setPhase(int newPhase); //sets phase of hero animation void hover(bool on) override; void clickLeft(tribool down, bool previousState) override; //call-in void clickRight(tribool down, bool previousState) override; //call-in CBattleHero(const std::string & animationPath, bool filpG, PlayerColor player, const CGHeroInstance * hero, const CBattleInterface * owner); ~CBattleHero(); }; class CHeroInfoWindow : public CWindowObject { private: std::vector> labels; std::vector> icons; public: CHeroInfoWindow(const InfoAboutHero & hero, Point * position); }; /// Class which manages the battle options window class CBattleOptionsWindow : public WindowBase { private: std::shared_ptr background; std::shared_ptr setToDefault; std::shared_ptr exit; std::shared_ptr animSpeeds; std::vector> labels; std::vector> toggles; public: CBattleOptionsWindow(const SDL_Rect & position, CBattleInterface * owner); void bDefaultf(); //default button callback void bExitf(); //exit button callback }; /// Class which is responsible for showing the battle result window class CBattleResultWindow : public WindowBase { private: std::shared_ptr background; std::vector> labels; std::shared_ptr exit; std::vector> icons; std::shared_ptr description; CPlayerInterface & owner; public: CBattleResultWindow(const BattleResult & br, CPlayerInterface & _owner); ~CBattleResultWindow(); void bExitf(); //exit button callback void activate() override; void show(SDL_Surface * to = 0) override; }; /// Class which stands for a single hex field on a battlefield class CClickableHex : public CIntObject { private: bool setAlterText; //if true, this hex has set alternative text in console and will clean it public: ui32 myNumber; //number of hex in commonly used format bool accessible; //if true, this hex is accessible for units //CStack * ourStack; bool strictHovered; //for determining if hex is hovered by mouse (this is different problem than hex's graphic hovering) CBattleInterface * myInterface; //interface that owns me static Point getXYUnitAnim(BattleHex hexNum, const CStack * creature, CBattleInterface * cbi); //returns (x, y) of left top corner of animation //for user interactions void hover (bool on) override; void mouseMoved (const SDL_MouseMotionEvent &sEvent) override; void clickLeft(tribool down, bool previousState) override; void clickRight(tribool down, bool previousState) override; CClickableHex(); }; /// Shows the stack queue class CStackQueue : public CIntObject { class StackBox : public CIntObject { public: std::shared_ptr background; std::shared_ptr icon; std::shared_ptr amount; std::shared_ptr stateIcon; void setUnit(const battle::Unit * unit, size_t turn = 0); StackBox(CStackQueue * owner); }; static const int QUEUE_SIZE = 10; std::shared_ptr background; std::vector> stackBoxes; CBattleInterface * owner; std::shared_ptr icons; std::shared_ptr stateIcons; public: const bool embedded; CStackQueue(bool Embedded, CBattleInterface * _owner); ~CStackQueue(); void update(); }; vcmi-0.99+git20190113.f06c8a87/client/battle/CCreatureAnimation.cpp000066400000000000000000000224411342332007200241270ustar00rootroot00000000000000/* * CCreatureAnimation.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CCreatureAnimation.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" #include "../gui/SDL_Extensions.h" static const SDL_Color creatureBlueBorder = { 0, 255, 255, 255 }; static const SDL_Color creatureGoldBorder = { 255, 255, 0, 255 }; static const SDL_Color creatureNoBorder = { 0, 0, 0, 0 }; SDL_Color AnimationControls::getBlueBorder() { return creatureBlueBorder; } SDL_Color AnimationControls::getGoldBorder() { return creatureGoldBorder; } SDL_Color AnimationControls::getNoBorder() { return creatureNoBorder; } std::shared_ptr AnimationControls::getAnimation(const CCreature * creature) { auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2); return std::make_shared(creature->animDefName, func); } float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CCreatureAnimation * anim, size_t group) { CCreatureAnim::EAnimType type = CCreatureAnim::EAnimType(group); assert(creature->animation.walkAnimationTime != 0); assert(creature->animation.attackAnimationTime != 0); assert(anim->framesInGroup(type) != 0); // possible new fields for creature format: //split "Attack time" into "Shoot Time" and "Cast Time" // a lot of arbitrary multipliers, mostly to make animation speed closer to H3 const float baseSpeed = 0.1; const float speedMult = settings["battle"]["animationSpeed"].Float(); const float speed = baseSpeed / speedMult; switch (type) { case CCreatureAnim::MOVING: return speed * 2 * creature->animation.walkAnimationTime / anim->framesInGroup(type); case CCreatureAnim::MOUSEON: return baseSpeed; case CCreatureAnim::HOLDING: return baseSpeed * creature->animation.idleAnimationTime / anim->framesInGroup(type); case CCreatureAnim::SHOOT_UP: case CCreatureAnim::SHOOT_FRONT: case CCreatureAnim::SHOOT_DOWN: case CCreatureAnim::CAST_UP: case CCreatureAnim::CAST_FRONT: case CCreatureAnim::CAST_DOWN: case CCreatureAnim::VCMI_CAST_DOWN: case CCreatureAnim::VCMI_CAST_FRONT: case CCreatureAnim::VCMI_CAST_UP: return speed * 4 * creature->animation.attackAnimationTime / anim->framesInGroup(type); // as strange as it looks like "attackAnimationTime" does not affects melee attacks // necessary because length of these animations must be same for all creatures for synchronization case CCreatureAnim::ATTACK_UP: case CCreatureAnim::ATTACK_FRONT: case CCreatureAnim::ATTACK_DOWN: case CCreatureAnim::HITTED: case CCreatureAnim::DEFENCE: case CCreatureAnim::DEATH: case CCreatureAnim::DEATH_RANGED: case CCreatureAnim::VCMI_2HEX_DOWN: case CCreatureAnim::VCMI_2HEX_FRONT: case CCreatureAnim::VCMI_2HEX_UP: return speed * 3 / anim->framesInGroup(type); case CCreatureAnim::TURN_L: case CCreatureAnim::TURN_R: return speed / 3; case CCreatureAnim::MOVE_START: case CCreatureAnim::MOVE_END: return speed / 3; case CCreatureAnim::DEAD: case CCreatureAnim::DEAD_RANGED: return speed; default: return speed; } } float AnimationControls::getProjectileSpeed() { return settings["battle"]["animationSpeed"].Float() * 100; } float AnimationControls::getSpellEffectSpeed() { return settings["battle"]["animationSpeed"].Float() * 60; } float AnimationControls::getMovementDuration(const CCreature * creature) { return settings["battle"]["animationSpeed"].Float() * 4 / creature->animation.walkAnimationTime; } float AnimationControls::getFlightDistance(const CCreature * creature) { return creature->animation.flightAnimationDistance * 200; } CCreatureAnim::EAnimType CCreatureAnimation::getType() const { return type; } void CCreatureAnimation::setType(CCreatureAnim::EAnimType type) { this->type = type; currentFrame = 0; once = false; play(); } CCreatureAnimation::CCreatureAnimation(const std::string & name_, TSpeedController controller) : name(name_), speed(0.1), currentFrame(0), elapsedTime(0), type(CCreatureAnim::HOLDING), border(CSDL_Ext::makeColor(0, 0, 0, 0)), speedController(controller), once(false) { forward = std::make_shared(name_); reverse = std::make_shared(name_); //todo: optimize forward->preload(); reverse->preload(); // if necessary, add one frame into vcmi-only group DEAD if(forward->size(CCreatureAnim::DEAD) == 0) { forward->duplicateImage(CCreatureAnim::DEATH, forward->size(CCreatureAnim::DEATH)-1, CCreatureAnim::DEAD); reverse->duplicateImage(CCreatureAnim::DEATH, reverse->size(CCreatureAnim::DEATH)-1, CCreatureAnim::DEAD); } if(forward->size(CCreatureAnim::DEAD_RANGED) == 0 && forward->size(CCreatureAnim::DEATH_RANGED) != 0) { forward->duplicateImage(CCreatureAnim::DEATH_RANGED, forward->size(CCreatureAnim::DEATH_RANGED)-1, CCreatureAnim::DEAD_RANGED); reverse->duplicateImage(CCreatureAnim::DEATH_RANGED, reverse->size(CCreatureAnim::DEATH_RANGED)-1, CCreatureAnim::DEAD_RANGED); } //TODO: get dimensions form CAnimation auto first = forward->getImage(0, type, true); if(!first) { fullWidth = 0; fullHeight = 0; return; } fullWidth = first->width(); fullHeight = first->height(); reverse->verticalFlip(); play(); } void CCreatureAnimation::endAnimation() { once = false; auto copy = onAnimationReset; onAnimationReset.clear(); copy(); } bool CCreatureAnimation::incrementFrame(float timePassed) { elapsedTime += timePassed; currentFrame += timePassed * speed; if (currentFrame >= float(framesInGroup(type))) { // just in case of extremely low fps (or insanely high speed) while (currentFrame >= float(framesInGroup(type))) currentFrame -= framesInGroup(type); if (once) setType(CCreatureAnim::HOLDING); endAnimation(); return true; } return false; } void CCreatureAnimation::setBorderColor(SDL_Color palette) { border = palette; } int CCreatureAnimation::getWidth() const { return fullWidth; } int CCreatureAnimation::getHeight() const { return fullHeight; } float CCreatureAnimation::getCurrentFrame() const { return currentFrame; } void CCreatureAnimation::playOnce( CCreatureAnim::EAnimType type ) { setType(type); once = true; } inline int getBorderStrength(float time) { float borderStrength = fabs(vstd::round(time) - time) * 2; // generate value in range 0-1 return borderStrength * 155 + 100; // scale to 0-255 } static SDL_Color genShadow(ui8 alpha) { return CSDL_Ext::makeColor(0, 0, 0, alpha); } static SDL_Color genBorderColor(ui8 alpha, const SDL_Color & base) { return CSDL_Ext::makeColor(base.r, base.g, base.b, ui8(base.a * alpha / 256)); } static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2) { return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256; } static SDL_Color addColors(const SDL_Color & base, const SDL_Color & over) { return CSDL_Ext::makeColor( mixChannels(over.r, base.r, over.a, base.a), mixChannels(over.g, base.g, over.a, base.a), mixChannels(over.b, base.b, over.a, base.a), ui8(over.a + base.a * (255 - over.a) / 256) ); } void CCreatureAnimation::genBorderPalette(IImage::BorderPallete & target) { target[0] = genBorderColor(getBorderStrength(elapsedTime), border); target[1] = addColors(genShadow(128), genBorderColor(getBorderStrength(elapsedTime), border)); target[2] = addColors(genShadow(64), genBorderColor(getBorderStrength(elapsedTime), border)); } void CCreatureAnimation::nextFrame(SDL_Surface * dest, bool attacker) { size_t frame = floor(currentFrame); std::shared_ptr image; if(attacker) image = forward->getImage(frame, type); else image = reverse->getImage(frame, type); if(image) { IImage::BorderPallete borderPallete; genBorderPalette(borderPallete); image->setBorderPallete(borderPallete); image->draw(dest, pos.x, pos.y); } } int CCreatureAnimation::framesInGroup(CCreatureAnim::EAnimType group) const { return forward->size(group); } bool CCreatureAnimation::isDead() const { return getType() == CCreatureAnim::DEAD || getType() == CCreatureAnim::DEATH || getType() == CCreatureAnim::DEAD_RANGED || getType() == CCreatureAnim::DEATH_RANGED; } bool CCreatureAnimation::isIdle() const { return getType() == CCreatureAnim::HOLDING || getType() == CCreatureAnim::MOUSEON; } bool CCreatureAnimation::isMoving() const { return getType() == CCreatureAnim::MOVE_START || getType() == CCreatureAnim::MOVING || getType() == CCreatureAnim::MOVE_END; } bool CCreatureAnimation::isShooting() const { return getType() == CCreatureAnim::SHOOT_UP || getType() == CCreatureAnim::SHOOT_FRONT || getType() == CCreatureAnim::SHOOT_DOWN; } void CCreatureAnimation::pause() { speed = 0; } void CCreatureAnimation::play() { //logAnim->trace("Play %s group %d at %d:%d", name, static_cast(getType()), pos.x, pos.y); speed = 0; if(speedController(this, type) != 0) speed = 1 / speedController(this, type); } vcmi-0.99+git20190113.f06c8a87/client/battle/CCreatureAnimation.h000066400000000000000000000077031342332007200236000ustar00rootroot00000000000000/* * CCreatureAnimation.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/FunctionList.h" #include "../widgets/Images.h" #include "../gui/CAnimation.h" class CIntObject; class CCreatureAnimation; /// Namespace for some common controls of animations namespace AnimationControls { /// get SDL_Color for creature selection borders SDL_Color getBlueBorder(); SDL_Color getGoldBorder(); SDL_Color getNoBorder(); /// creates animation object with preset speed control std::shared_ptr getAnimation(const CCreature * creature); /// returns animation speed of specific group, taking in mind game setting (in frames per second) float getCreatureAnimationSpeed(const CCreature * creature, const CCreatureAnimation * anim, size_t groupID); /// returns how far projectile should move each frame /// TODO: make it time-based float getProjectileSpeed(); /// returns speed of any spell effects, including any special effects like morale (in frames per second) float getSpellEffectSpeed(); /// returns duration of full movement animation, in seconds. Needed to move animation on screen float getMovementDuration(const CCreature * creature); /// Returns distance on which flying creatures should during one animation loop float getFlightDistance(const CCreature * creature); } /// Class which manages animations of creatures/units inside battles /// TODO: split into constant image container and class that does *control* of animation class CCreatureAnimation : public CIntObject { public: typedef std::function TSpeedController; private: std::string name; std::shared_ptr forward; std::shared_ptr reverse; int fullWidth; int fullHeight; // speed of animation, measure in frames per second float speed; // currently displayed frame. Float to allow H3-style animations where frames // don't display for integer number of frames float currentFrame; // cumulative, real-time duration of animation. Used for effects like selection border float elapsedTime; CCreatureAnim::EAnimType type; //type of animation being displayed // border color, disabled if alpha = 0 SDL_Color border; TSpeedController speedController; bool once; // animation will be played once and the reset to idling void endAnimation(); void genBorderPalette(IImage::BorderPallete & target); public: // function(s) that will be called when animation ends, after reset to 1st frame // NOTE that these function will be fired only once CFunctionList onAnimationReset; int getWidth() const; int getHeight() const; /// Constructor /// name - path to .def file, relative to SPRITES/ directory /// controller - function that will return for how long *each* frame /// in specified group of animation should be played, measured in seconds CCreatureAnimation(const std::string & name_, TSpeedController speedController); void setType(CCreatureAnim::EAnimType type); //sets type of animation and cleares framecount CCreatureAnim::EAnimType getType() const; //returns type of animation void nextFrame(SDL_Surface * dest, bool attacker); // should be called every frame, return true when animation was reset to beginning bool incrementFrame(float timePassed); void setBorderColor(SDL_Color palette); float getCurrentFrame() const; // Gets the current frame ID relative to frame group. void playOnce(CCreatureAnim::EAnimType type); //plays once given stage of animation, then resets to 2 int framesInGroup(CCreatureAnim::EAnimType group) const; void pause(); void play(); //helpers. TODO: move them somewhere else bool isDead() const; bool isIdle() const; bool isMoving() const; bool isShooting() const; }; vcmi-0.99+git20190113.f06c8a87/client/gui/000077500000000000000000000000001342332007200172145ustar00rootroot00000000000000vcmi-0.99+git20190113.f06c8a87/client/gui/CAnimation.cpp000066400000000000000000000723461342332007200217560ustar00rootroot00000000000000/* * CAnimation.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CAnimation.h" #include #include "../CBitmapHandler.h" #include "../Graphics.h" #include "../gui/SDL_Extensions.h" #include "../gui/SDL_Pixels.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/ISimpleResourceLoader.h" #include "../lib/JsonNode.h" #include "../lib/CRandomGenerator.h" class SDLImageLoader; typedef std::map > source_map; typedef std::map image_map; typedef std::map group_map; /// Class for def loading /// After loading will store general info (palette and frame offsets) and pointer to file itself class CDefFile { private: struct SSpriteDef { ui32 size; ui32 format; /// format in which pixel data is stored ui32 fullWidth; /// full width and height of frame, including borders ui32 fullHeight; ui32 width; /// width and height of pixel data, borders excluded ui32 height; si32 leftMargin; si32 topMargin; } PACKED_STRUCT; //offset[group][frame] - offset of frame data in file std::map > offset; std::unique_ptr data; std::unique_ptr palette; public: CDefFile(std::string Name); ~CDefFile(); //load frame as SDL_Surface template void loadFrame(size_t frame, size_t group, ImageLoader &loader) const; const std::map getEntries() const; }; /* * Wrapper around SDL_Surface */ class SDLImage : public IImage { public: //Surface without empty borders SDL_Surface * surf; //size of left and top borders Point margins; //total size including borders Point fullSize; public: //Load image from def file SDLImage(CDefFile *data, size_t frame, size_t group=0); //Load from bitmap file SDLImage(std::string filename); SDLImage(const JsonNode & conf); //Create using existing surface, extraRef will increase refcount on SDL_Surface SDLImage(SDL_Surface * from, bool extraRef); ~SDLImage(); void draw(SDL_Surface * where, int posX=0, int posY=0, Rect *src=nullptr, ui8 alpha=255) const override; void draw(SDL_Surface * where, SDL_Rect * dest, SDL_Rect * src, ui8 alpha=255) const override; std::shared_ptr scaleFast(float scale) const override; void exportBitmap(const boost::filesystem::path & path) const override; void playerColored(PlayerColor player) override; void setFlagColor(PlayerColor player) override; int width() const override; int height() const override; void horizontalFlip() override; void verticalFlip() override; void shiftPalette(int from, int howMany) override; void setBorderPallete(const BorderPallete & borderPallete) override; friend class SDLImageLoader; }; class SDLImageLoader { SDLImage * image; ui8 * lineStart; ui8 * position; public: //load size raw pixels from data inline void Load(size_t size, const ui8 * data); //set size pixels to color inline void Load(size_t size, ui8 color=0); inline void EndLine(); //init image with these sizes and palette inline void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal); SDLImageLoader(SDLImage * Img); ~SDLImageLoader(); }; // Extremely simple file cache. TODO: smarter, more general solution class CFileCache { static const int cacheSize = 50; //Max number of cached files struct FileData { ResourceID name; size_t size; std::unique_ptr data; std::unique_ptr getCopy() { auto ret = std::unique_ptr(new ui8[size]); std::copy(data.get(), data.get() + size, ret.get()); return ret; } FileData(ResourceID name_, size_t size_, std::unique_ptr data_): name{std::move(name_)}, size{size_}, data{std::move(data_)} {} }; std::deque cache; public: std::unique_ptr getCachedFile(ResourceID rid) { for(auto & file : cache) { if (file.name == rid) return file.getCopy(); } // Still here? Cache miss if (cache.size() > cacheSize) cache.pop_front(); auto data = CResourceHandler::get()->load(rid)->readAll(); cache.emplace_back(std::move(rid), data.second, std::move(data.first)); return cache.back().getCopy(); } }; enum class DefType : uint32_t { SPELL = 0x40, SPRITE = 0x41, CREATURE = 0x42, MAP = 0x43, MAP_HERO = 0x44, TERRAIN = 0x45, CURSOR = 0x46, INTERFACE = 0x47, SPRITE_FRAME = 0x48, BATTLE_HERO = 0x49 }; static CFileCache animationCache; /************************************************************************* * DefFile, class used for def loading * *************************************************************************/ bool operator== (const SDL_Color & lhs, const SDL_Color & rhs) { return (lhs.a == rhs.a) && (lhs.b == rhs.b) &&(lhs.g == rhs.g) &&(lhs.r == rhs.r); } CDefFile::CDefFile(std::string Name): data(nullptr), palette(nullptr) { #if 0 static SDL_Color H3_ORIG_PALETTE[8] = { { 0, 255, 255, SDL_ALPHA_OPAQUE}, {255, 150, 255, SDL_ALPHA_OPAQUE}, {255, 100, 255, SDL_ALPHA_OPAQUE}, {255, 50, 255, SDL_ALPHA_OPAQUE}, {255, 0, 255, SDL_ALPHA_OPAQUE}, {255, 255, 0, SDL_ALPHA_OPAQUE}, {180, 0, 255, SDL_ALPHA_OPAQUE}, { 0, 255, 0, SDL_ALPHA_OPAQUE} }; #endif // 0 //First 8 colors in def palette used for transparency static SDL_Color H3Palette[8] = { { 0, 0, 0, 0},// 100% - transparency { 0, 0, 0, 32},// 75% - shadow border, { 0, 0, 0, 64},// TODO: find exact value { 0, 0, 0, 128},// TODO: for transparency { 0, 0, 0, 128},// 50% - shadow body { 0, 0, 0, 0},// 100% - selection highlight { 0, 0, 0, 128},// 50% - shadow body below selection { 0, 0, 0, 64} // 75% - shadow border below selection }; data = animationCache.getCachedFile(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION)); palette = std::unique_ptr(new SDL_Color[256]); int it = 0; ui32 type = read_le_u32(data.get() + it); it+=4; //int width = read_le_u32(data + it); it+=4;//not used //int height = read_le_u32(data + it); it+=4; it+=8; ui32 totalBlocks = read_le_u32(data.get() + it); it+=4; for (ui32 i= 0; i<256; i++) { palette[i].r = data[it++]; palette[i].g = data[it++]; palette[i].b = data[it++]; palette[i].a = SDL_ALPHA_OPAQUE; } switch(static_cast(type)) { case DefType::SPELL: palette[0] = H3Palette[0]; break; case DefType::SPRITE: case DefType::SPRITE_FRAME: for(ui32 i= 0; i<8; i++) palette[i] = H3Palette[i]; break; case DefType::CREATURE: palette[0] = H3Palette[0]; palette[1] = H3Palette[1]; palette[4] = H3Palette[4]; palette[5] = H3Palette[5]; palette[6] = H3Palette[6]; palette[7] = H3Palette[7]; break; case DefType::MAP: case DefType::MAP_HERO: palette[0] = H3Palette[0]; palette[1] = H3Palette[1]; palette[4] = H3Palette[4]; //5 = owner flag, handled separately break; case DefType::TERRAIN: palette[0] = H3Palette[0]; palette[1] = H3Palette[1]; palette[2] = H3Palette[2]; palette[3] = H3Palette[3]; palette[4] = H3Palette[4]; break; case DefType::CURSOR: palette[0] = H3Palette[0]; break; case DefType::INTERFACE: palette[0] = H3Palette[0]; palette[1] = H3Palette[1]; palette[4] = H3Palette[4]; //player colors handled separately //TODO: disallow colorizing other def types break; case DefType::BATTLE_HERO: palette[0] = H3Palette[0]; palette[1] = H3Palette[1]; palette[4] = H3Palette[4]; break; default: logAnim->error("Unknown def type %d in %s", type, Name); break; } for (ui32 i=0; i void CDefFile::loadFrame(size_t frame, size_t group, ImageLoader &loader) const { std::map >::const_iterator it; it = offset.find(group); assert (it != offset.end()); const ui8 * FDef = data.get()+it->second[frame]; const SSpriteDef sd = * reinterpret_cast(FDef); SSpriteDef sprite; sprite.format = read_le_u32(&sd.format); sprite.fullWidth = read_le_u32(&sd.fullWidth); sprite.fullHeight = read_le_u32(&sd.fullHeight); sprite.width = read_le_u32(&sd.width); sprite.height = read_le_u32(&sd.height); sprite.leftMargin = read_le_u32(&sd.leftMargin); sprite.topMargin = read_le_u32(&sd.topMargin); ui32 currentOffset = sizeof(SSpriteDef); //special case for some "old" format defs (SGTWMTA.DEF and SGTWMTB.DEF) if(sprite.format == 1 && sprite.width > sprite.fullWidth && sprite.height > sprite.fullHeight) { sprite.leftMargin = 0; sprite.topMargin = 0; sprite.width = sprite.fullWidth; sprite.height = sprite.fullHeight; currentOffset -= 16; } const ui32 BaseOffset = currentOffset; loader.init(Point(sprite.width, sprite.height), Point(sprite.leftMargin, sprite.topMargin), Point(sprite.fullWidth, sprite.fullHeight), palette.get()); switch(sprite.format) { case 0: { //pixel data is not compressed, copy data to surface for(ui32 i=0; i(FDef+currentOffset); currentOffset += sizeof(ui32) * sprite.height; for(ui32 i=0; ierror("Error: unsupported format of def file: %d", sprite.format); break; } } CDefFile::~CDefFile() = default; const std::map CDefFile::getEntries() const { std::map ret; for (auto & elem : offset) ret[elem.first] = elem.second.size(); return ret; } /************************************************************************* * Classes for image loaders - helpers for loading from def files * *************************************************************************/ SDLImageLoader::SDLImageLoader(SDLImage * Img): image(Img), lineStart(nullptr), position(nullptr) { } void SDLImageLoader::init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) { //Init image image->surf = SDL_CreateRGBSurface(0, SpriteSize.x, SpriteSize.y, 8, 0, 0, 0, 0); image->margins = Margins; image->fullSize = FullSize; //Prepare surface SDL_Palette * p = SDL_AllocPalette(256); SDL_SetPaletteColors(p, pal, 0, 256); SDL_SetSurfacePalette(image->surf, p); SDL_FreePalette(p); SDL_LockSurface(image->surf); lineStart = position = (ui8*)image->surf->pixels; } inline void SDLImageLoader::Load(size_t size, const ui8 * data) { if (size) { memcpy((void *)position, data, size); position += size; } } inline void SDLImageLoader::Load(size_t size, ui8 color) { if (size) { memset((void *)position, color, size); position += size; } } inline void SDLImageLoader::EndLine() { lineStart += image->surf->pitch; position = lineStart; } SDLImageLoader::~SDLImageLoader() { SDL_UnlockSurface(image->surf); SDL_SetColorKey(image->surf, SDL_TRUE, 0); //TODO: RLE if compressed and bpp>1 } /************************************************************************* * Classes for images, support loading from file and drawing on surface * *************************************************************************/ IImage::IImage() = default; IImage::~IImage() = default; SDLImage::SDLImage(CDefFile * data, size_t frame, size_t group) : surf(nullptr), margins(0, 0), fullSize(0, 0) { SDLImageLoader loader(this); data->loadFrame(frame, group, loader); } SDLImage::SDLImage(SDL_Surface * from, bool extraRef) : surf(nullptr), margins(0, 0), fullSize(0, 0) { surf = from; if (extraRef) surf->refcount++; fullSize.x = surf->w; fullSize.y = surf->h; } SDLImage::SDLImage(const JsonNode & conf) : surf(nullptr), margins(0, 0), fullSize(0, 0) { std::string filename = conf["file"].String(); surf = BitmapHandler::loadBitmap(filename); if(surf == nullptr) return; const JsonNode & jsonMargins = conf["margins"]; margins.x = jsonMargins["left"].Integer(); margins.y = jsonMargins["top"].Integer(); fullSize.x = conf["width"].Integer(); fullSize.y = conf["height"].Integer(); if(fullSize.x == 0) { fullSize.x = margins.x + surf->w + jsonMargins["right"].Integer(); } if(fullSize.y == 0) { fullSize.y = margins.y + surf->h + jsonMargins["bottom"].Integer(); } } SDLImage::SDLImage(std::string filename) : surf(nullptr), margins(0, 0), fullSize(0, 0) { surf = BitmapHandler::loadBitmap(filename); if(surf == nullptr) { logGlobal->error("Error: failed to load image %s", filename); return; } else { fullSize.x = surf->w; fullSize.y = surf->h; } } void SDLImage::draw(SDL_Surface *where, int posX, int posY, Rect *src, ui8 alpha) const { if(!surf) return; Rect destRect(posX, posY, surf->w, surf->h); draw(where, &destRect, src); } void SDLImage::draw(SDL_Surface* where, SDL_Rect* dest, SDL_Rect* src, ui8 alpha) const { if (!surf) return; Rect sourceRect(0, 0, surf->w, surf->h); Point destShift(0, 0); if(src) { if(src->x < margins.x) destShift.x += margins.x - src->x; if(src->y < margins.y) destShift.y += margins.y - src->y; sourceRect = Rect(*src) & Rect(margins.x, margins.y, surf->w, surf->h); sourceRect -= margins; } else destShift = margins; Rect destRect(destShift.x, destShift.y, surf->w, surf->h); if(dest) { destRect.x += dest->x; destRect.y += dest->y; } if(surf->format->BitsPerPixel == 8) { CSDL_Ext::blit8bppAlphaTo24bpp(surf, &sourceRect, where, &destRect); } else { SDL_UpperBlit(surf, &sourceRect, where, &destRect); } } std::shared_ptr SDLImage::scaleFast(float scale) const { auto scaled = CSDL_Ext::scaleSurfaceFast(surf, surf->w * scale, surf->h * scale); if (scaled->format && scaled->format->palette) // fix color keying, because SDL loses it at this point CSDL_Ext::setColorKey(scaled, scaled->format->palette->colors[0]); else if(scaled->format && scaled->format->Amask) SDL_SetSurfaceBlendMode(scaled, SDL_BLENDMODE_BLEND);//just in case else CSDL_Ext::setDefaultColorKey(scaled);//just in case SDLImage * ret = new SDLImage(scaled, false); ret->fullSize.x = (int) round((float)fullSize.x * scale); ret->fullSize.y = (int) round((float)fullSize.y * scale); ret->margins.x = (int) round((float)margins.x * scale); ret->margins.y = (int) round((float)margins.y * scale); return std::shared_ptr(ret); } void SDLImage::exportBitmap(const boost::filesystem::path& path) const { SDL_SaveBMP(surf, path.string().c_str()); } void SDLImage::playerColored(PlayerColor player) { graphics->blueToPlayersAdv(surf, player); } void SDLImage::setFlagColor(PlayerColor player) { if(player < PlayerColor::PLAYER_LIMIT || player==PlayerColor::NEUTRAL) CSDL_Ext::setPlayerColor(surf, player); } int SDLImage::width() const { return fullSize.x; } int SDLImage::height() const { return fullSize.y; } void SDLImage::horizontalFlip() { margins.y = fullSize.y - surf->h - margins.y; //todo: modify in-place SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf); SDL_FreeSurface(surf); surf = flipped; } void SDLImage::verticalFlip() { margins.x = fullSize.x - surf->w - margins.x; //todo: modify in-place SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf); SDL_FreeSurface(surf); surf = flipped; } void SDLImage::shiftPalette(int from, int howMany) { //works with at most 16 colors, if needed more -> increase values assert(howMany < 16); if(surf->format->palette) { SDL_Color palette[16]; for(int i=0; iformat->palette->colors[from + i]; } SDL_SetColors(surf, palette, from, howMany); } } void SDLImage::setBorderPallete(const IImage::BorderPallete & borderPallete) { if(surf->format->palette) { SDL_SetColors(surf, const_cast(borderPallete.data()), 5, 3); } } SDLImage::~SDLImage() { SDL_FreeSurface(surf); } std::shared_ptr CAnimation::getFromExtraDef(std::string filename) { size_t pos = filename.find(':'); if (pos == -1) return nullptr; CAnimation anim(filename.substr(0, pos)); pos++; size_t frame = atoi(filename.c_str()+pos); size_t group = 0; pos = filename.find(':', pos); if (pos != -1) { pos++; group = frame; frame = atoi(filename.c_str()+pos); } anim.load(frame ,group); auto ret = anim.images[group][frame]; anim.images.clear(); return ret; } bool CAnimation::loadFrame(size_t frame, size_t group) { if(size(group) <= frame) { printError(frame, group, "LoadFrame"); return false; } auto image = getImage(frame, group, false); if(image) { return true; } //try to get image from def if(source[group][frame].getType() == JsonNode::JsonType::DATA_NULL) { if(defFile) { auto frameList = defFile->getEntries(); if(vstd::contains(frameList, group) && frameList.at(group) > frame) // frame is present { images[group][frame] = std::make_shared(defFile.get(), frame, group); return true; } } // still here? image is missing printError(frame, group, "LoadFrame"); images[group][frame] = std::make_shared("DEFAULT"); } else //load from separate file { auto img = getFromExtraDef(source[group][frame]["file"].String()); if(!img) img = std::make_shared(source[group][frame]); images[group][frame] = img; return true; } return false; } bool CAnimation::unloadFrame(size_t frame, size_t group) { auto image = getImage(frame, group, false); if(image) { images[group].erase(frame); if(images[group].empty()) images.erase(group); return true; } return false; } void CAnimation::initFromJson(const JsonNode & config) { std::string basepath; basepath = config["basepath"].String(); JsonNode base(JsonNode::JsonType::DATA_STRUCT); base["margins"] = config["margins"]; base["width"] = config["width"]; base["height"] = config["height"]; for(const JsonNode & group : config["sequences"].Vector()) { size_t groupID = group["group"].Integer();//TODO: string-to-value conversion("moving" -> MOVING) source[groupID].clear(); for(const JsonNode & frame : group["frames"].Vector()) { JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); JsonUtils::inherit(toAdd, base); toAdd["file"].String() = basepath + frame.String(); source[groupID].push_back(toAdd); } } for(const JsonNode & node : config["images"].Vector()) { size_t group = node["group"].Integer(); size_t frame = node["frame"].Integer(); if (source[group].size() <= frame) source[group].resize(frame+1); JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); JsonUtils::inherit(toAdd, base); toAdd["file"].String() = basepath + node["file"].String(); source[group][frame] = toAdd; } } void CAnimation::exportBitmaps(const boost::filesystem::path& path) const { if(images.empty()) { logGlobal->error("Nothing to export, animation is empty"); return; } boost::filesystem::path actualPath = path / "SPRITES" / name; boost::filesystem::create_directories(actualPath); size_t counter = 0; for(const auto & groupPair : images) { size_t group = groupPair.first; for(const auto & imagePair : groupPair.second) { size_t frame = imagePair.first; const auto img = imagePair.second; boost::format fmt("%d_%d.bmp"); fmt % group % frame; img->exportBitmap(actualPath / fmt.str()); counter++; } } logGlobal->info("Exported %d frames to %s", counter, actualPath.string()); } void CAnimation::init() { if(defFile) { const std::map defEntries = defFile->getEntries(); for (auto & defEntry : defEntries) source[defEntry.first].resize(defEntry.second); } ResourceID resID(std::string("SPRITES/") + name, EResType::TEXT); if (vstd::contains(graphics->imageLists, resID.getName())) initFromJson(graphics->imageLists[resID.getName()]); auto configList = CResourceHandler::get()->getResourcesWithName(resID); for(auto & loader : configList) { auto stream = loader->load(resID); std::unique_ptr textData(new ui8[stream->getSize()]); stream->read(textData.get(), stream->getSize()); const JsonNode config((char*)textData.get(), stream->getSize()); initFromJson(config); } } void CAnimation::printError(size_t frame, size_t group, std::string type) const { logGlobal->error("%s error: Request for frame not present in CAnimation! File name: %s, Group: %d, Frame: %d", type, name, group, frame); } CAnimation::CAnimation(std::string Name): name(Name), preloaded(false), defFile() { size_t dotPos = name.find_last_of('.'); if ( dotPos!=-1 ) name.erase(dotPos); std::transform(name.begin(), name.end(), name.begin(), toupper); ResourceID resource(std::string("SPRITES/") + name, EResType::ANIMATION); if(CResourceHandler::get()->existsResource(resource)) defFile = std::make_shared(name); init(); if(source.empty()) logAnim->error("Animation %s failed to load", Name); } CAnimation::CAnimation(): name(""), preloaded(false), defFile() { init(); } CAnimation::~CAnimation() = default; void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup) { if(!source.count(sourceGroup)) { logAnim->error("Group %d missing in %s", sourceGroup, name); return; } if(source[sourceGroup].size() <= sourceFrame) { logAnim->error("Frame [%d %d] missing in %s", sourceGroup, sourceFrame, name); return; } //todo: clone actual loaded Image object JsonNode clone(source[sourceGroup][sourceFrame]); if(clone.getType() == JsonNode::JsonType::DATA_NULL) { std::string temp = name+":"+boost::lexical_cast(sourceGroup)+":"+boost::lexical_cast(sourceFrame); clone["file"].String() = temp; } source[targetGroup].push_back(clone); size_t index = source[targetGroup].size() - 1; if(preloaded) load(index, targetGroup); } void CAnimation::setCustom(std::string filename, size_t frame, size_t group) { if (source[group].size() <= frame) source[group].resize(frame+1); source[group][frame]["file"].String() = filename; //FIXME: update image if already loaded } std::shared_ptr CAnimation::getImage(size_t frame, size_t group, bool verbose) const { auto groupIter = images.find(group); if (groupIter != images.end()) { auto imageIter = groupIter->second.find(frame); if (imageIter != groupIter->second.end()) return imageIter->second; } if (verbose) printError(frame, group, "GetImage"); return nullptr; } void CAnimation::load() { for (auto & elem : source) for (size_t image=0; image < elem.second.size(); image++) loadFrame(image, elem.first); } void CAnimation::unload() { for (auto & elem : source) for (size_t image=0; image < elem.second.size(); image++) unloadFrame(image, elem.first); } void CAnimation::preload() { if(!preloaded) { preloaded = true; load(); } } void CAnimation::loadGroup(size_t group) { if (vstd::contains(source, group)) for (size_t image=0; image < source[group].size(); image++) loadFrame(image, group); } void CAnimation::unloadGroup(size_t group) { if (vstd::contains(source, group)) for (size_t image=0; image < source[group].size(); image++) unloadFrame(image, group); } void CAnimation::load(size_t frame, size_t group) { loadFrame(frame, group); } void CAnimation::unload(size_t frame, size_t group) { unloadFrame(frame, group); } size_t CAnimation::size(size_t group) const { auto iter = source.find(group); if (iter != source.end()) return iter->second.size(); return 0; } void CAnimation::horizontalFlip() { for(auto & group : images) for(auto & image : group.second) image.second->horizontalFlip(); } void CAnimation::verticalFlip() { for(auto & group : images) for(auto & image : group.second) image.second->verticalFlip(); } void CAnimation::playerColored(PlayerColor player) { for(auto & group : images) for(auto & image : group.second) image.second->playerColored(player); } void CAnimation::createFlippedGroup(const size_t sourceGroup, const size_t targetGroup) { for(size_t frame = 0; frame < size(sourceGroup); ++frame) { duplicateImage(sourceGroup, frame, targetGroup); auto image = getImage(frame, targetGroup); image->verticalFlip(); } } float CFadeAnimation::initialCounter() const { if (fadingMode == EMode::OUT) return 1.0f; return 0.0f; } void CFadeAnimation::update() { if (!fading) return; if (fadingMode == EMode::OUT) fadingCounter -= delta; else fadingCounter += delta; if (isFinished()) { fading = false; if (shouldFreeSurface) { SDL_FreeSurface(fadingSurface); fadingSurface = nullptr; } } } bool CFadeAnimation::isFinished() const { if (fadingMode == EMode::OUT) return fadingCounter <= 0.0f; return fadingCounter >= 1.0f; } CFadeAnimation::CFadeAnimation() : delta(0), fadingSurface(nullptr), fading(false), fadingCounter(0), shouldFreeSurface(false), fadingMode(EMode::NONE) { } CFadeAnimation::~CFadeAnimation() { if (fadingSurface && shouldFreeSurface) SDL_FreeSurface(fadingSurface); } void CFadeAnimation::init(EMode mode, SDL_Surface * sourceSurface, bool freeSurfaceAtEnd, float animDelta) { if (fading) { // in that case, immediately finish the previous fade // (alternatively, we could just return here to ignore the new fade request until this one finished (but we'd need to free the passed bitmap to avoid leaks)) logGlobal->warn("Tried to init fading animation that is already running."); if (fadingSurface && shouldFreeSurface) SDL_FreeSurface(fadingSurface); } if (animDelta <= 0.0f) { logGlobal->warn("Fade anim: delta should be positive; %f given.", animDelta); animDelta = DEFAULT_DELTA; } if (sourceSurface) fadingSurface = sourceSurface; delta = animDelta; fadingMode = mode; fadingCounter = initialCounter(); fading = true; shouldFreeSurface = freeSurfaceAtEnd; } void CFadeAnimation::draw(SDL_Surface * targetSurface, const SDL_Rect * sourceRect, SDL_Rect * destRect) { if (!fading || !fadingSurface || fadingMode == EMode::NONE) { fading = false; return; } CSDL_Ext::setAlpha(fadingSurface, fadingCounter * 255); SDL_BlitSurface(fadingSurface, const_cast(sourceRect), targetSurface, destRect); //FIXME CSDL_Ext::setAlpha(fadingSurface, 255); } vcmi-0.99+git20190113.f06c8a87/client/gui/CAnimation.h000066400000000000000000000106621342332007200214140ustar00rootroot00000000000000/* * CAnimation.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include "../../lib/vcmi_endian.h" #include "Geometries.h" #include "../../lib/GameConstants.h" struct SDL_Surface; class JsonNode; class CDefFile; /* * Base class for images, can be used for non-animation pictures as well */ class IImage { public: using BorderPallete = std::array; //draws image on surface "where" at position virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, Rect * src = nullptr, ui8 alpha = 255) const=0; virtual void draw(SDL_Surface * where, SDL_Rect * dest, SDL_Rect * src, ui8 alpha = 255) const = 0; virtual std::shared_ptr scaleFast(float scale) const = 0; virtual void exportBitmap(const boost::filesystem::path & path) const = 0; //Change palette to specific player virtual void playerColored(PlayerColor player)=0; //set special color for flag virtual void setFlagColor(PlayerColor player)=0; virtual int width() const=0; virtual int height() const=0; //only indexed bitmaps, 16 colors maximum virtual void shiftPalette(int from, int howMany) = 0; //only indexed bitmaps, colors 5,6,7 must be special virtual void setBorderPallete(const BorderPallete & borderPallete) = 0; virtual void horizontalFlip() = 0; virtual void verticalFlip() = 0; IImage(); virtual ~IImage(); }; /// Class for handling animation class CAnimation { private: //source[group][position] - file with this frame, if string is empty - image located in def file std::map > source; //bitmap[group][position], store objects with loaded bitmaps std::map > > images; //animation file name std::string name; bool preloaded; std::shared_ptr defFile; //loader, will be called by load(), require opened def file for loading from it. Returns true if image is loaded bool loadFrame(size_t frame, size_t group); //unloadFrame, returns true if image has been unloaded ( either deleted or decreased refCount) bool unloadFrame(size_t frame, size_t group); //initialize animation from file void initFromJson(const JsonNode & input); void init(); //to get rid of copy-pasting error message :] void printError(size_t frame, size_t group, std::string type) const; //not a very nice method to get image from another def file //TODO: remove after implementing resource manager std::shared_ptr getFromExtraDef(std::string filename); public: CAnimation(std::string Name); CAnimation(); ~CAnimation(); //duplicates frame at [sourceGroup, sourceFrame] as last frame in targetGroup //and loads it if animation is preloaded void duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup); //add custom surface to the selected position. void setCustom(std::string filename, size_t frame, size_t group=0); std::shared_ptr getImage(size_t frame, size_t group=0, bool verbose=true) const; void exportBitmaps(const boost::filesystem::path & path) const; //all available frames void load (); void unload(); void preload(); //all frames from group void loadGroup (size_t group); void unloadGroup(size_t group); //single image void load (size_t frame, size_t group=0); void unload(size_t frame, size_t group=0); //total count of frames in group (including not loaded) size_t size(size_t group=0) const; void horizontalFlip(); void verticalFlip(); void playerColored(PlayerColor player); void createFlippedGroup(const size_t sourceGroup, const size_t targetGroup); }; const float DEFAULT_DELTA = 0.05f; class CFadeAnimation { public: enum class EMode { NONE, IN, OUT }; private: float delta; SDL_Surface * fadingSurface; bool fading; float fadingCounter; bool shouldFreeSurface; float initialCounter() const; bool isFinished() const; public: EMode fadingMode; CFadeAnimation(); ~CFadeAnimation(); void init(EMode mode, SDL_Surface * sourceSurface, bool freeSurfaceAtEnd = false, float animDelta = DEFAULT_DELTA); void update(); void draw(SDL_Surface * targetSurface, const SDL_Rect * sourceRect, SDL_Rect * destRect); bool isFading() const { return fading; } }; vcmi-0.99+git20190113.f06c8a87/client/gui/CCursorHandler.cpp000066400000000000000000000117031342332007200226000ustar00rootroot00000000000000/* * CCursorHandler.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CCursorHandler.h" #include #include "SDL_Extensions.h" #include "CGuiHandler.h" #include "../widgets/Images.h" #include "../CMT.h" void CCursorHandler::clearBuffer() { Uint32 fillColor = SDL_MapRGBA(buffer->format, 0, 0, 0, 0); CSDL_Ext::fillRect(buffer, nullptr, fillColor); } void CCursorHandler::updateBuffer(CIntObject * payload) { payload->moveTo(Point(0,0)); payload->showAll(buffer); needUpdate = true; } void CCursorHandler::replaceBuffer(CIntObject * payload) { clearBuffer(); updateBuffer(payload); } void CCursorHandler::initCursor() { cursorLayer = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 40, 40); SDL_SetTextureBlendMode(cursorLayer, SDL_BLENDMODE_BLEND); xpos = ypos = 0; type = ECursor::DEFAULT; dndObject = nullptr; cursors = { make_unique("CRADVNTR", 0), make_unique("CRCOMBAT", 0), make_unique("CRDEFLT", 0), make_unique("CRSPELL", 0) }; currentCursor = cursors.at(int(ECursor::DEFAULT)).get(); buffer = CSDL_Ext::newSurface(40,40); SDL_SetSurfaceBlendMode(buffer, SDL_BLENDMODE_NONE); SDL_ShowCursor(SDL_DISABLE); changeGraphic(ECursor::ADVENTURE, 0); } void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index) { if(type != this->type) { this->type = type; this->frame = index; currentCursor = cursors.at(int(type)).get(); currentCursor->setFrame(index); } else if(index != this->frame) { this->frame = index; currentCursor->setFrame(index); } replaceBuffer(currentCursor); } void CCursorHandler::dragAndDropCursor(std::unique_ptr object) { dndObject = std::move(object); if(dndObject) replaceBuffer(dndObject.get()); else replaceBuffer(currentCursor); } void CCursorHandler::cursorMove(const int & x, const int & y) { xpos = x; ypos = y; } void CCursorHandler::shiftPos( int &x, int &y ) { if(( type == ECursor::COMBAT && frame != ECursor::COMBAT_POINTER) || type == ECursor::SPELLBOOK) { x-=16; y-=16; // Properly align the melee attack cursors. if (type == ECursor::COMBAT) { switch (frame) { case 7: // Bottom left x -= 6; y += 16; break; case 8: // Left x -= 16; y += 10; break; case 9: // Top left x -= 6; y -= 6; break; case 10: // Top right x += 16; y -= 6; break; case 11: // Right x += 16; y += 11; break; case 12: // Bottom right x += 16; y += 16; break; case 13: // Below x += 9; y += 16; break; case 14: // Above x += 9; y -= 15; break; } } } else if(type == ECursor::ADVENTURE) { if (frame == 0); //to exclude else if(frame == 2) { x -= 12; y -= 10; } else if(frame == 3) { x -= 12; y -= 12; } else if(frame < 27) { int hlpNum = (frame - 4)%6; if(hlpNum == 0) { x -= 15; y -= 13; } else if(hlpNum == 1) { x -= 13; y -= 13; } else if(hlpNum == 2) { x -= 20; y -= 20; } else if(hlpNum == 3) { x -= 13; y -= 16; } else if(hlpNum == 4) { x -= 8; y -= 9; } else if(hlpNum == 5) { x -= 14; y -= 16; } } else if(frame == 41) { x -= 14; y -= 16; } else if(frame < 31 || frame == 42) { x -= 20; y -= 20; } } } void CCursorHandler::centerCursor() { this->xpos = (screen->w / 2.) - (currentCursor->pos.w / 2.); this->ypos = (screen->h / 2.) - (currentCursor->pos.h / 2.); SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE); SDL_WarpMouse(this->xpos, this->ypos); SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE); } void CCursorHandler::render() { if(!showing) return; //the must update texture in the main (renderer) thread, but changes to cursor type may come from other threads updateTexture(); int x = xpos; int y = ypos; shiftPos(x, y); if(dndObject) { x -= dndObject->pos.w/2; y -= dndObject->pos.h/2; } SDL_Rect destRect; destRect.x = x; destRect.y = y; destRect.w = 40; destRect.h = 40; SDL_RenderCopy(mainRenderer, cursorLayer, nullptr, &destRect); } void CCursorHandler::updateTexture() { if(needUpdate) { SDL_UpdateTexture(cursorLayer, nullptr, buffer->pixels, buffer->pitch); needUpdate = false; } } CCursorHandler::CCursorHandler() : needUpdate(true), buffer(nullptr), cursorLayer(nullptr), showing(false) { } CCursorHandler::~CCursorHandler() { if(buffer) SDL_FreeSurface(buffer); if(cursorLayer) SDL_DestroyTexture(cursorLayer); } vcmi-0.99+git20190113.f06c8a87/client/gui/CCursorHandler.h000066400000000000000000000041431342332007200222450ustar00rootroot00000000000000/* * CCursorHandler.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once class CIntObject; class CAnimImage; struct SDL_Surface; struct SDL_Texture; namespace ECursor { enum ECursorTypes { ADVENTURE, COMBAT, DEFAULT, SPELLBOOK }; enum EBattleCursors { COMBAT_BLOCKED, COMBAT_MOVE, COMBAT_FLY, COMBAT_SHOOT, COMBAT_HERO, COMBAT_QUERY, COMBAT_POINTER, //various attack frames COMBAT_SHOOT_PENALTY = 15, COMBAT_SHOOT_CATAPULT, COMBAT_HEAL, COMBAT_SACRIFICE, COMBAT_TELEPORT}; } /// handles mouse cursor class CCursorHandler final { bool needUpdate; SDL_Texture * cursorLayer; SDL_Surface * buffer; CAnimImage * currentCursor; std::unique_ptr dndObject; //if set, overrides currentCursor std::array, 4> cursors; bool showing; void clearBuffer(); void updateBuffer(CIntObject * payload); void replaceBuffer(CIntObject * payload); void shiftPos( int &x, int &y ); void updateTexture(); public: /// position of cursor int xpos, ypos; /// Current cursor ECursor::ECursorTypes type; size_t frame; /// inits cursorHandler - run only once, it's not memleak-proof (rev 1333) void initCursor(); /// changes cursor graphic for type type (0 - adventure, 1 - combat, 2 - default, 3 - spellbook) and frame index (not used for type 3) void changeGraphic(ECursor::ECursorTypes type, int index); /** * Replaces the cursor with a custom image. * * @param image Image to replace cursor with or nullptr to use the normal * cursor. CursorHandler takes ownership of object */ void dragAndDropCursor (std::unique_ptr image); void render(); void hide() { showing=false; }; void show() { showing=true; }; /// change cursor's positions to (x, y) void cursorMove(const int & x, const int & y); /// Move cursor to screen center void centerCursor(); CCursorHandler(); ~CCursorHandler(); }; vcmi-0.99+git20190113.f06c8a87/client/gui/CGuiHandler.cpp000066400000000000000000000403021342332007200220440ustar00rootroot00000000000000/* * CGuiHandler.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CGuiHandler.h" #include "../lib/CondSh.h" #include #include "CIntObject.h" #include "CCursorHandler.h" #include "../CGameInfo.h" #include "../../lib/CThreadHelper.h" #include "../../lib/CConfigHandler.h" #include "../CMT.h" #include "../CPlayerInterface.h" #include "../battle/CBattleInterface.h" extern std::queue events; extern boost::mutex eventsM; boost::thread_specific_ptr inGuiThread; SObjectConstruction::SObjectConstruction(CIntObject *obj) :myObj(obj) { GH.createdObj.push_front(obj); GH.captureChildren = true; } SObjectConstruction::~SObjectConstruction() { assert(GH.createdObj.size()); assert(GH.createdObj.front() == myObj); GH.createdObj.pop_front(); GH.captureChildren = GH.createdObj.size(); } SSetCaptureState::SSetCaptureState(bool allow, ui8 actions) { previousCapture = GH.captureChildren; GH.captureChildren = false; prevActions = GH.defActionsDef; GH.defActionsDef = actions; } SSetCaptureState::~SSetCaptureState() { GH.captureChildren = previousCapture; GH.defActionsDef = prevActions; } static inline void processList(const ui16 mask, const ui16 flag, std::list *lst, std::function *)> cb) { if (mask & flag) cb(lst); } void CGuiHandler::processLists(const ui16 activityFlag, std::function *)> cb) { processList(CIntObject::LCLICK,activityFlag,&lclickable,cb); processList(CIntObject::RCLICK,activityFlag,&rclickable,cb); processList(CIntObject::MCLICK,activityFlag,&mclickable,cb); processList(CIntObject::HOVER,activityFlag,&hoverable,cb); processList(CIntObject::MOVE,activityFlag,&motioninterested,cb); processList(CIntObject::KEYBOARD,activityFlag,&keyinterested,cb); processList(CIntObject::TIME,activityFlag,&timeinterested,cb); processList(CIntObject::WHEEL,activityFlag,&wheelInterested,cb); processList(CIntObject::DOUBLECLICK,activityFlag,&doubleClickInterested,cb); processList(CIntObject::TEXTINPUT,activityFlag,&textInterested,cb); } void CGuiHandler::handleElementActivate(CIntObject * elem, ui16 activityFlag) { processLists(activityFlag,[&](std::list * lst){ lst->push_front(elem); }); elem->active_m |= activityFlag; } void CGuiHandler::handleElementDeActivate(CIntObject * elem, ui16 activityFlag) { processLists(activityFlag,[&](std::list * lst){ auto hlp = std::find(lst->begin(),lst->end(),elem); assert(hlp != lst->end()); lst->erase(hlp); }); elem->active_m &= ~activityFlag; } void CGuiHandler::popInt(std::shared_ptr top) { assert(listInt.front() == top); top->deactivate(); disposed.push_back(top); listInt.pop_front(); objsToBlit -= top; if(!listInt.empty()) listInt.front()->activate(); totalRedraw(); pushSDLEvent(SDL_USEREVENT, EUserEvent::INTERFACE_CHANGED); } void CGuiHandler::pushInt(std::shared_ptr newInt) { assert(newInt); assert(!vstd::contains(listInt, newInt)); // do not add same object twice //a new interface will be present, we'll need to use buffer surface (unless it's advmapint that will alter screenBuf on activate anyway) screenBuf = screen2; if(!listInt.empty()) listInt.front()->deactivate(); listInt.push_front(newInt); CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); newInt->activate(); objsToBlit.push_back(newInt); totalRedraw(); pushSDLEvent(SDL_USEREVENT, EUserEvent::INTERFACE_CHANGED); } void CGuiHandler::popInts(int howMany) { if(!howMany) return; //senseless but who knows... assert(listInt.size() >= howMany); listInt.front()->deactivate(); for(int i=0; i < howMany; i++) { objsToBlit -= listInt.front(); disposed.push_back(listInt.front()); listInt.pop_front(); } if(!listInt.empty()) { listInt.front()->activate(); totalRedraw(); } fakeMouseMove(); pushSDLEvent(SDL_USEREVENT, EUserEvent::INTERFACE_CHANGED); } std::shared_ptr CGuiHandler::topInt() { if(listInt.empty()) return std::shared_ptr(); else return listInt.front(); } void CGuiHandler::totalRedraw() { for(auto & elem : objsToBlit) elem->showAll(screen2); blitAt(screen2,0,0,screen); } void CGuiHandler::updateTime() { int ms = mainFPSmng->getElapsedMilliseconds(); std::list hlp = timeinterested; for (auto & elem : hlp) { if(!vstd::contains(timeinterested,elem)) continue; (elem)->onTimer(ms); } } void CGuiHandler::handleEvents() { //player interface may want special event handling if(nullptr != LOCPLINT && LOCPLINT->capturedAllEvents()) return; boost::unique_lock lock(eventsM); while(!events.empty()) { continueEventHandling = true; SDL_Event ev = events.front(); current = &ev; events.pop(); handleCurrentEvent(); } } void CGuiHandler::handleCurrentEvent() { if(current->type == SDL_KEYDOWN || current->type == SDL_KEYUP) { SDL_KeyboardEvent key = current->key; if(current->type == SDL_KEYDOWN && key.keysym.sym >= SDLK_F1 && key.keysym.sym <= SDLK_F15 && settings["session"]["spectate"].Bool()) { //TODO: we need some central place for all interface-independent hotkeys Settings s = settings.write["session"]; switch(key.keysym.sym) { case SDLK_F5: if(settings["session"]["spectate-locked-pim"].Bool()) LOCPLINT->pim->unlock(); else LOCPLINT->pim->lock(); s["spectate-locked-pim"].Bool() = !settings["session"]["spectate-locked-pim"].Bool(); break; case SDLK_F6: s["spectate-ignore-hero"].Bool() = !settings["session"]["spectate-ignore-hero"].Bool(); break; case SDLK_F7: s["spectate-skip-battle"].Bool() = !settings["session"]["spectate-skip-battle"].Bool(); break; case SDLK_F8: s["spectate-skip-battle-result"].Bool() = !settings["session"]["spectate-skip-battle-result"].Bool(); break; case SDLK_F9: //not working yet since CClient::run remain locked after CBattleInterface removal // if(LOCPLINT->battleInt) // { // GH.popInts(1); // vstd::clear_pointer(LOCPLINT->battleInt); // } break; default: break; } return; } //translate numpad keys if(key.keysym.sym == SDLK_KP_ENTER) { key.keysym.sym = SDLK_RETURN; key.keysym.scancode = SDL_SCANCODE_RETURN; } bool keysCaptured = false; for(auto i = keyinterested.begin(); i != keyinterested.end() && continueEventHandling; i++) { if((*i)->captureThisEvent(key)) { keysCaptured = true; break; } } std::list miCopy = keyinterested; for(auto i = miCopy.begin(); i != miCopy.end() && continueEventHandling; i++) if(vstd::contains(keyinterested,*i) && (!keysCaptured || (*i)->captureThisEvent(key))) (**i).keyPressed(key); } else if(current->type == SDL_MOUSEMOTION) { handleMouseMotion(); } else if(current->type == SDL_MOUSEBUTTONDOWN) { switch(current->button.button) { case SDL_BUTTON_LEFT: if(lastClick == current->motion && (SDL_GetTicks() - lastClickTime) < 300) { std::list hlp = doubleClickInterested; for(auto i = hlp.begin(); i != hlp.end() && continueEventHandling; i++) { if(!vstd::contains(doubleClickInterested, *i)) continue; if(isItIn(&(*i)->pos, current->motion.x, current->motion.y)) { (*i)->onDoubleClick(); } } } lastClick = current->motion; lastClickTime = SDL_GetTicks(); handleMouseButtonClick(lclickable, EIntObjMouseBtnType::LEFT, true); break; case SDL_BUTTON_RIGHT: handleMouseButtonClick(rclickable, EIntObjMouseBtnType::RIGHT, true); break; case SDL_BUTTON_MIDDLE: handleMouseButtonClick(mclickable, EIntObjMouseBtnType::MIDDLE, true); break; default: break; } } else if(current->type == SDL_MOUSEWHEEL) { std::list hlp = wheelInterested; for(auto i = hlp.begin(); i != hlp.end() && continueEventHandling; i++) { if(!vstd::contains(wheelInterested,*i)) continue; // SDL doesn't have the proper values for mouse positions on SDL_MOUSEWHEEL, refetch them int x = 0, y = 0; SDL_GetMouseState(&x, &y); (*i)->wheelScrolled(current->wheel.y < 0, isItIn(&(*i)->pos, x, y)); } } else if(current->type == SDL_TEXTINPUT) { for(auto it : textInterested) { it->textInputed(current->text); } } else if(current->type == SDL_TEXTEDITING) { for(auto it : textInterested) { it->textEdited(current->edit); } } //todo: muiltitouch else if(current->type == SDL_MOUSEBUTTONUP) { switch(current->button.button) { case SDL_BUTTON_LEFT: handleMouseButtonClick(lclickable, EIntObjMouseBtnType::LEFT, false); break; case SDL_BUTTON_RIGHT: handleMouseButtonClick(rclickable, EIntObjMouseBtnType::RIGHT, false); break; case SDL_BUTTON_MIDDLE: handleMouseButtonClick(mclickable, EIntObjMouseBtnType::MIDDLE, false); break; } } current = nullptr; } //event end void CGuiHandler::handleMouseButtonClick(CIntObjectList & interestedObjs, EIntObjMouseBtnType btn, bool isPressed) { auto hlp = interestedObjs; for(auto i = hlp.begin(); i != hlp.end() && continueEventHandling; i++) { if(!vstd::contains(interestedObjs, *i)) continue; auto prev = (*i)->mouseState(btn); if(!isPressed) (*i)->updateMouseState(btn, isPressed); if(isItIn(&(*i)->pos, current->motion.x, current->motion.y)) { if(isPressed) (*i)->updateMouseState(btn, isPressed); (*i)->click(btn, isPressed, prev); } else if(!isPressed) (*i)->click(btn, boost::logic::indeterminate, prev); } } void CGuiHandler::handleMouseMotion() { //sending active, hovered hoverable objects hover() call std::vector hlp; for(auto & elem : hoverable) { if(isItIn(&(elem)->pos, current->motion.x, current->motion.y)) { if (!(elem)->hovered) hlp.push_back((elem)); } else if ((elem)->hovered) { (elem)->hover(false); (elem)->hovered = false; } } for(auto & elem : hlp) { elem->hover(true); elem->hovered = true; } handleMoveInterested(current->motion); } void CGuiHandler::simpleRedraw() { //update only top interface and draw background if(objsToBlit.size() > 1) blitAt(screen2,0,0,screen); //blit background if(!objsToBlit.empty()) objsToBlit.back()->show(screen); //blit active interface/window } void CGuiHandler::handleMoveInterested(const SDL_MouseMotionEvent & motion) { //sending active, MotionInterested objects mouseMoved() call std::list miCopy = motioninterested; for(auto & elem : miCopy) { if(elem->strongInterest || isItInOrLowerBounds(&elem->pos, motion.x, motion.y)) //checking lower bounds fixes bug #2476 { (elem)->mouseMoved(motion); } } } void CGuiHandler::fakeMouseMove() { SDL_Event event; SDL_MouseMotionEvent sme = {SDL_MOUSEMOTION, 0, 0, 0, 0, 0, 0, 0, 0}; int x, y; sme.state = SDL_GetMouseState(&x, &y); sme.x = x; sme.y = y; event.motion = sme; SDL_PushEvent(&event); } void CGuiHandler::renderFrame() { // Updating GUI requires locking pim mutex (that protects screen and GUI state). // During game: // When ending the game, the pim mutex might be hold by other thread, // that will notify us about the ending game by setting terminate_cond flag. //in PreGame terminate_cond stay false bool acquiredTheLockOnPim = false; //for tracking whether pim mutex locking succeeded while(!terminate_cond->get() && !(acquiredTheLockOnPim = CPlayerInterface::pim->try_lock())) //try acquiring long until it succeeds or we are told to terminate boost::this_thread::sleep(boost::posix_time::milliseconds(15)); if(acquiredTheLockOnPim) { // If we are here, pim mutex has been successfully locked - let's store it in a safe RAII lock. boost::unique_lock un(*CPlayerInterface::pim, boost::adopt_lock); if(nullptr != curInt) curInt->update(); if(settings["general"]["showfps"].Bool()) drawFPSCounter(); SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch); SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr); CCS->curh->render(); SDL_RenderPresent(mainRenderer); disposed.clear(); } mainFPSmng->framerateDelay(); // holds a constant FPS } CGuiHandler::CGuiHandler() : lastClick(-500, -500),lastClickTime(0), defActionsDef(0), captureChildren(false) { continueEventHandling = true; curInt = nullptr; current = nullptr; statusbar = nullptr; // Creates the FPS manager and sets the framerate to 48 which is doubled the value of the original Heroes 3 FPS rate mainFPSmng = new CFramerateManager(48); //do not init CFramerateManager here --AVS terminate_cond = new CondSh(false); } CGuiHandler::~CGuiHandler() { delete mainFPSmng; delete terminate_cond; } void CGuiHandler::breakEventHandling() { continueEventHandling = false; } void CGuiHandler::drawFPSCounter() { const static SDL_Color yellow = {255, 255, 0, 0}; static SDL_Rect overlay = { 0, 0, 64, 32}; Uint32 black = SDL_MapRGB(screen->format, 10, 10, 10); SDL_FillRect(screen, &overlay, black); std::string fps = boost::lexical_cast(mainFPSmng->fps); graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, yellow, Point(10, 10)); } SDL_Keycode CGuiHandler::arrowToNum(SDL_Keycode key) { switch(key) { case SDLK_DOWN: return SDLK_KP_2; case SDLK_UP: return SDLK_KP_8; case SDLK_LEFT: return SDLK_KP_4; case SDLK_RIGHT: return SDLK_KP_6; default: throw std::runtime_error("Wrong key!"); } } SDL_Keycode CGuiHandler::numToDigit(SDL_Keycode key) { #define REMOVE_KP(keyName) case SDLK_KP_ ## keyName : return SDLK_ ## keyName; switch(key) { REMOVE_KP(0) REMOVE_KP(1) REMOVE_KP(2) REMOVE_KP(3) REMOVE_KP(4) REMOVE_KP(5) REMOVE_KP(6) REMOVE_KP(7) REMOVE_KP(8) REMOVE_KP(9) REMOVE_KP(PERIOD) REMOVE_KP(MINUS) REMOVE_KP(PLUS) REMOVE_KP(EQUALS) case SDLK_KP_MULTIPLY: return SDLK_ASTERISK; case SDLK_KP_DIVIDE: return SDLK_SLASH; case SDLK_KP_ENTER: return SDLK_RETURN; default: return SDLK_UNKNOWN; } #undef REMOVE_KP } bool CGuiHandler::isNumKey(SDL_Keycode key, bool number) { if(number) return key >= SDLK_KP_1 && key <= SDLK_KP_0; else return (key >= SDLK_KP_1 && key <= SDLK_KP_0) || key == SDLK_KP_MINUS || key == SDLK_KP_PLUS || key == SDLK_KP_EQUALS; } bool CGuiHandler::isArrowKey(SDL_Keycode key) { return key == SDLK_UP || key == SDLK_DOWN || key == SDLK_LEFT || key == SDLK_RIGHT; } bool CGuiHandler::amIGuiThread() { return inGuiThread.get() && *inGuiThread; } void CGuiHandler::pushSDLEvent(int type, int usercode) { SDL_Event event; event.type = type; event.user.code = usercode; // not necessarily used SDL_PushEvent(&event); } CFramerateManager::CFramerateManager(int rate) { this->rate = rate; this->rateticks = (1000.0 / rate); this->fps = 0; this->accumulatedFrames = 0; this->accumulatedTime = 0; this->lastticks = 0; this->timeElapsed = 0; } void CFramerateManager::init() { this->lastticks = SDL_GetTicks(); } void CFramerateManager::framerateDelay() { ui32 currentTicks = SDL_GetTicks(); timeElapsed = currentTicks - lastticks; // FPS is higher than it should be, then wait some time if (timeElapsed < rateticks) { SDL_Delay(ceil(this->rateticks) - timeElapsed); } accumulatedTime += timeElapsed; accumulatedFrames++; if(accumulatedFrames >= 100) { //about 2 second should be passed fps = ceil(1000.0 / (accumulatedTime/accumulatedFrames)); accumulatedTime = 0; accumulatedFrames = 0; } currentTicks = SDL_GetTicks(); // recalculate timeElapsed for external calls via getElapsed() // limit it to 1000 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint) timeElapsed = std::min(currentTicks - lastticks, 1000); lastticks = SDL_GetTicks(); } vcmi-0.99+git20190113.f06c8a87/client/gui/CGuiHandler.h000066400000000000000000000133321342332007200215140ustar00rootroot00000000000000/* * CGuiHandler.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once //#include "../../lib/CStopWatch.h" #include "Geometries.h" #include "SDL_Extensions.h" class CFramerateManager; class CGStatusBar; class CIntObject; class IUpdateable; class IShowActivatable; class IShowable; enum class EIntObjMouseBtnType; template struct CondSh; // TODO: event handling need refactoring enum EUserEvent { /*CHANGE_SCREEN_RESOLUTION = 1,*/ RETURN_TO_MAIN_MENU = 2, //STOP_CLIENT = 3, RESTART_GAME = 4, RETURN_TO_MENU_LOAD, FULLSCREEN_TOGGLED, CAMPAIGN_START_SCENARIO, FORCE_QUIT, //quit client without question INTERFACE_CHANGED }; // A fps manager which holds game updates at a constant rate class CFramerateManager { private: double rateticks; ui32 lastticks, timeElapsed; int rate; ui32 accumulatedTime,accumulatedFrames; public: int fps; // the actual fps value CFramerateManager(int rate); // initializes the manager with a given fps rate void init(); // needs to be called directly before the main game loop to reset the internal timer void framerateDelay(); // needs to be called every game update cycle ui32 getElapsedMilliseconds() const {return this->timeElapsed;} }; // Handles GUI logic and drawing class CGuiHandler { public: CFramerateManager * mainFPSmng; //to keep const framerate std::list> listInt; //list of interfaces - front=foreground; back = background (includes adventure map, window interfaces, all kind of active dialogs, and so on) std::shared_ptr statusbar; private: std::vector> disposed; std::atomic continueEventHandling; typedef std::list CIntObjectList; //active GUI elements (listening for events CIntObjectList lclickable, rclickable, mclickable, hoverable, keyinterested, motioninterested, timeinterested, wheelInterested, doubleClickInterested, textInterested; void handleMouseButtonClick(CIntObjectList & interestedObjs, EIntObjMouseBtnType btn, bool isPressed); void processLists(const ui16 activityFlag, std::function *)> cb); public: void handleElementActivate(CIntObject * elem, ui16 activityFlag); void handleElementDeActivate(CIntObject * elem, ui16 activityFlag); public: //objs to blit std::vector> objsToBlit; SDL_Event * current; //current event - can be set to nullptr to stop handling event IUpdateable *curInt; Point lastClick; unsigned lastClickTime; ui8 defActionsDef; //default auto actions bool captureChildren; //all newly created objects will get their parents from stack and will be added to parents children list std::list createdObj; //stack of objs being created CGuiHandler(); ~CGuiHandler(); void renderFrame(); void totalRedraw(); //forces total redraw (using showAll), sets a flag, method gets called at the end of the rendering void simpleRedraw(); //update only top interface and draw background from buffer, sets a flag, method gets called at the end of the rendering void pushInt(std::shared_ptr newInt); //deactivate old top interface, activates this one and pushes to the top template void pushIntT(Args && ... args) { auto newInt = std::make_shared(std::forward(args)...); pushInt(newInt); } void popInts(int howMany); //pops one or more interfaces - deactivates top, deletes and removes given number of interfaces, activates new front void popInt(std::shared_ptr top); //removes given interface from the top and activates next std::shared_ptr topInt(); //returns top interface void updateTime(); //handles timeInterested void handleEvents(); //takes events from queue and calls interested objects void handleCurrentEvent(); void handleMouseMotion(); void handleMoveInterested( const SDL_MouseMotionEvent & motion ); void fakeMouseMove(); void breakEventHandling(); //current event won't be propagated anymore void drawFPSCounter(); // draws the FPS to the upper left corner of the screen static SDL_Keycode arrowToNum(SDL_Keycode key); //converts arrow key to according numpad key static SDL_Keycode numToDigit(SDL_Keycode key);//converts numpad digit key to normal digit key static bool isNumKey(SDL_Keycode key, bool number = true); //checks if key is on numpad (numbers - check only for numpad digits) static bool isArrowKey(SDL_Keycode key); static bool amIGuiThread(); static void pushSDLEvent(int type, int usercode = 0); CondSh * terminate_cond; // confirm termination }; extern CGuiHandler GH; //global gui handler struct SObjectConstruction { CIntObject *myObj; SObjectConstruction(CIntObject *obj); ~SObjectConstruction(); }; struct SSetCaptureState { bool previousCapture; ui8 prevActions; SSetCaptureState(bool allow, ui8 actions); ~SSetCaptureState(); }; #define OBJ_CONSTRUCTION SObjectConstruction obj__i(this) #define OBJECT_CONSTRUCTION_CAPTURING(actions) defActions = actions; SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this) #define OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(actions) SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this) #define OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE defActions = 255 - DISPOSE; SSetCaptureState obj__i1(true, 255 - DISPOSE); SObjectConstruction obj__i(this) vcmi-0.99+git20190113.f06c8a87/client/gui/CIntObject.cpp000066400000000000000000000176501342332007200217150ustar00rootroot00000000000000/* * CIntObject.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CIntObject.h" #include "CGuiHandler.h" #include "SDL_Extensions.h" #include "../CMessage.h" IShowActivatable::IShowActivatable() { type = 0; } CIntObject::CIntObject(int used_, Point pos_): parent_m(nullptr), active_m(0), parent(parent_m), active(active_m) { hovered = captureAllKeys = strongInterest = false; toNextTick = timerDelay = 0; used = used_; recActions = defActions = GH.defActionsDef; pos.x = pos_.x; pos.y = pos_.y; pos.w = 0; pos.h = 0; if(GH.captureChildren) GH.createdObj.front()->addChild(this, true); } CIntObject::~CIntObject() { if(active_m) deactivate(); while(!children.empty()) { if((defActions & DISPOSE) && (children.front()->recActions & DISPOSE)) delete children.front(); else removeChild(children.front()); } if(parent_m) parent_m->removeChild(this); } void CIntObject::setTimer(int msToTrigger) { if (!(active & TIME)) activate(TIME); toNextTick = timerDelay = msToTrigger; used |= TIME; } void CIntObject::onTimer(int timePassed) { toNextTick -= timePassed; if (toNextTick < 0) { toNextTick += timerDelay; tick(); } } void CIntObject::show(SDL_Surface * to) { if(defActions & UPDATE) for(auto & elem : children) if(elem->recActions & UPDATE) elem->show(to); } void CIntObject::showAll(SDL_Surface * to) { if(defActions & SHOWALL) { for(auto & elem : children) if(elem->recActions & SHOWALL) elem->showAll(to); } } void CIntObject::activate() { if (active_m) { if ((used | GENERAL) == active_m) return; else { logGlobal->warn("Warning: IntObject re-activated with mismatching used and active"); deactivate(); //FIXME: better to avoid such possibility at all } } active_m |= GENERAL; activate(used); if(defActions & ACTIVATE) for(auto & elem : children) if(elem->recActions & ACTIVATE) elem->activate(); } void CIntObject::activate(ui16 what) { GH.handleElementActivate(this, what); } void CIntObject::deactivate() { if (!active_m) return; active_m &= ~ GENERAL; deactivate(active_m); assert(!active_m); if(defActions & DEACTIVATE) for(auto & elem : children) if(elem->recActions & DEACTIVATE) elem->deactivate(); } void CIntObject::deactivate(ui16 what) { GH.handleElementDeActivate(this, what); } void CIntObject::click(EIntObjMouseBtnType btn, tribool down, bool previousState) { switch(btn) { default: case EIntObjMouseBtnType::LEFT: clickLeft(down, previousState); break; case EIntObjMouseBtnType::MIDDLE: clickMiddle(down, previousState); break; case EIntObjMouseBtnType::RIGHT: clickRight(down, previousState); break; } } void CIntObject::printAtLoc(const std::string & text, int x, int y, EFonts font, SDL_Color kolor, SDL_Surface * dst) { graphics->fonts[font]->renderTextLeft(dst, text, kolor, Point(pos.x + x, pos.y + y)); } void CIntObject::printAtMiddleLoc(const std::string & text, int x, int y, EFonts font, SDL_Color kolor, SDL_Surface * dst) { printAtMiddleLoc(text, Point(x,y), font, kolor, dst); } void CIntObject::printAtMiddleLoc(const std::string & text, const Point &p, EFonts font, SDL_Color kolor, SDL_Surface * dst) { graphics->fonts[font]->renderTextCenter(dst, text, kolor, pos.topLeft() + p); } void CIntObject::blitAtLoc( SDL_Surface * src, int x, int y, SDL_Surface * dst ) { blitAt(src, pos.x + x, pos.y + y, dst); } void CIntObject::blitAtLoc(SDL_Surface * src, const Point &p, SDL_Surface * dst) { blitAtLoc(src, p.x, p.y, dst); } void CIntObject::printAtMiddleWBLoc( const std::string & text, int x, int y, EFonts font, int charpr, SDL_Color kolor, SDL_Surface * dst) { graphics->fonts[font]->renderTextLinesCenter(dst, CMessage::breakText(text, charpr, font), kolor, Point(pos.x + x, pos.y + y)); } void CIntObject::addUsedEvents(ui16 newActions) { if (active_m) activate(~used & newActions); used |= newActions; } void CIntObject::removeUsedEvents(ui16 newActions) { if (active_m) deactivate(used & newActions); used &= ~newActions; } void CIntObject::disable() { if(active) deactivate(); recActions = DISPOSE; } void CIntObject::enable() { if(!active_m && (!parent_m || parent_m->active)) activate(); recActions = 255; } bool CIntObject::isItInLoc( const SDL_Rect &rect, int x, int y ) { return isItIn(&rect, x - pos.x, y - pos.y); } bool CIntObject::isItInLoc( const SDL_Rect &rect, const Point &p ) { return isItIn(&rect, p.x - pos.x, p.y - pos.y); } void CIntObject::fitToScreen(int borderWidth, bool propagate) { Point newPos = pos.topLeft(); vstd::amax(newPos.x, borderWidth); vstd::amax(newPos.y, borderWidth); vstd::amin(newPos.x, screen->w - borderWidth - pos.w); vstd::amin(newPos.y, screen->h - borderWidth - pos.h); if (newPos != pos.topLeft()) moveTo(newPos, propagate); } void CIntObject::moveBy(const Point & p, bool propagate) { pos.x += p.x; pos.y += p.y; if(propagate) for(auto & elem : children) elem->moveBy(p, propagate); } void CIntObject::moveTo(const Point & p, bool propagate) { moveBy(Point(p.x - pos.x, p.y - pos.y), propagate); } void CIntObject::addChild(CIntObject * child, bool adjustPosition) { if (vstd::contains(children, child)) { return; } if (child->parent_m) { child->parent_m->removeChild(child, adjustPosition); } children.push_back(child); child->parent_m = this; if(adjustPosition) child->pos += pos; if (!active && child->active) child->deactivate(); if (active && !child->active) child->activate(); } void CIntObject::removeChild(CIntObject * child, bool adjustPosition) { if (!child) return; if(!vstd::contains(children, child)) throw std::runtime_error("Wrong child object"); if(child->parent_m != this) throw std::runtime_error("Wrong child object"); children -= child; child->parent_m = nullptr; if(adjustPosition) child->pos -= pos; } void CIntObject::redraw() { //currently most of calls come from active objects so this check won't affect them //it should fix glitches when called by inactive elements located below active window if (active) { if (parent_m && (type & REDRAW_PARENT)) { parent_m->redraw(); } else { showAll(screenBuf); if(screenBuf != screen) showAll(screen); } } } const Rect & CIntObject::center( const Rect &r, bool propagate ) { pos.w = r.w; pos.h = r.h; return center(Point(screen->w/2, screen->h/2), propagate); } const Rect & CIntObject::center( bool propagate ) { return center(pos, propagate); } const Rect & CIntObject::center(const Point & p, bool propagate) { moveBy(Point(p.x - pos.w/2 - pos.x, p.y - pos.h/2 - pos.y), propagate); return pos; } bool CIntObject::captureThisEvent(const SDL_KeyboardEvent & key) { return captureAllKeys; } CKeyShortcut::CKeyShortcut() {} CKeyShortcut::CKeyShortcut(int key) { if (key != SDLK_UNKNOWN) assignedKeys.insert(key); } CKeyShortcut::CKeyShortcut(std::set Keys) :assignedKeys(Keys) {} void CKeyShortcut::keyPressed(const SDL_KeyboardEvent & key) { if(vstd::contains(assignedKeys,key.keysym.sym) || vstd::contains(assignedKeys, CGuiHandler::numToDigit(key.keysym.sym))) { bool prev = mouseState(EIntObjMouseBtnType::LEFT); updateMouseState(EIntObjMouseBtnType::LEFT, key.state == SDL_PRESSED); clickLeft(key.state == SDL_PRESSED, prev); } } WindowBase::WindowBase(int used_, Point pos_) : CIntObject(used_, pos_) { } void WindowBase::close() { if(GH.topInt().get() != this) logGlobal->error("Only top interface must be closed"); GH.popInts(1); } vcmi-0.99+git20190113.f06c8a87/client/gui/CIntObject.h000066400000000000000000000167041342332007200213610ustar00rootroot00000000000000/* * CIntObject.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include #include "Geometries.h" #include "../Graphics.h" struct SDL_Surface; class CGuiHandler; class CPicture; struct SDL_KeyboardEvent; using boost::logic::tribool; // Defines a activate/deactive method class IActivatable { public: virtual void activate()=0; virtual void deactivate()=0; virtual ~IActivatable(){}; }; class IUpdateable { public: virtual void update()=0; virtual ~IUpdateable(){}; }; // Defines a show method class IShowable { public: virtual void redraw()=0; virtual void show(SDL_Surface * to) = 0; virtual void showAll(SDL_Surface * to) { show(to); } virtual ~IShowable(){}; }; class IShowActivatable : public IShowable, public IActivatable { public: //redraw parent flag - this int may be semi-transparent and require redraw of parent window enum {BLOCK_ADV_HOTKEYS = 2, REDRAW_PARENT=8}; int type; //bin flags using etype IShowActivatable(); virtual ~IShowActivatable(){}; }; enum class EIntObjMouseBtnType { LEFT, MIDDLE, RIGHT }; //typedef ui16 ActivityFlag; // Base UI element class CIntObject : public IShowActivatable //interface object { ui16 used;//change via addUsed() or delUsed //time handling int toNextTick; int timerDelay; std::map currentMouseState; void onTimer(int timePassed); //non-const versions of fields to allow changing them in CIntObject CIntObject *parent_m; //parent object ui16 active_m; protected: //activate or deactivate specific action (LCLICK, RCLICK...) void activate(ui16 what); void deactivate(ui16 what); public: /* * Functions and fields that supposed to be private but are not for now. * Don't use them unless you really know what they are for */ std::vector children; /* * Public interface */ /// read-only parent access. May not be a "clean" solution but allows some compatibility CIntObject * const & parent; /// position of object on the screen. Please do not modify this anywhere but in constructor - use moveBy\moveTo instead /*const*/ Rect pos; CIntObject(int used=0, Point offset=Point()); virtual ~CIntObject(); void updateMouseState(EIntObjMouseBtnType btn, bool state) { currentMouseState[btn] = state; } bool mouseState(EIntObjMouseBtnType btn) const { return currentMouseState.count(btn) ? currentMouseState.at(btn) : false; } virtual void click(EIntObjMouseBtnType btn, tribool down, bool previousState); virtual void clickLeft(tribool down, bool previousState) {} virtual void clickRight(tribool down, bool previousState) {} virtual void clickMiddle(tribool down, bool previousState) {} //hover handling /*const*/ bool hovered; //for determining if object is hovered virtual void hover (bool on){} //keyboard handling bool captureAllKeys; //if true, only this object should get info about pressed keys virtual void keyPressed(const SDL_KeyboardEvent & key){} virtual bool captureThisEvent(const SDL_KeyboardEvent & key); //allows refining captureAllKeys against specific events (eg. don't capture ENTER) virtual void textInputed(const SDL_TextInputEvent & event){}; virtual void textEdited(const SDL_TextEditingEvent & event){}; //mouse movement handling bool strongInterest; //if true - report all mouse movements, if not - only when hovered virtual void mouseMoved (const SDL_MouseMotionEvent & sEvent){} //time handling void setTimer(int msToTrigger);//set timer delay and activate timer if needed. virtual void tick(){} //mouse wheel virtual void wheelScrolled(bool down, bool in){} //double click virtual void onDoubleClick(){} enum {LCLICK=1, RCLICK=2, HOVER=4, MOVE=8, KEYBOARD=16, TIME=32, GENERAL=64, WHEEL=128, DOUBLECLICK=256, TEXTINPUT=512, MCLICK=1024, ALL=0xffff}; const ui16 & active; void addUsedEvents(ui16 newActions); void removeUsedEvents(ui16 newActions); enum {ACTIVATE=1, DEACTIVATE=2, UPDATE=4, SHOWALL=8, DISPOSE=16, SHARE_POS=32}; ui8 defActions; //which calls will be tried to be redirected to children ui8 recActions; //which calls we allow to receive from parent void disable(); //deactivates if needed, blocks all automatic activity, allows only disposal void enable(); //activates if needed, all activity enabled (Warning: may not be symetric with disable if recActions was limited!) // activate or deactivate object. Inactive object won't receive any input events (keyboard\mouse) // usually used automatically by parent void activate() override; void deactivate() override; //called each frame to update screen void show(SDL_Surface * to) override; //called on complete redraw only void showAll(SDL_Surface * to) override; //request complete redraw of this object void redraw() override; enum EAlignment {TOPLEFT, CENTER, BOTTOMRIGHT}; bool isItInLoc(const SDL_Rect &rect, int x, int y); bool isItInLoc(const SDL_Rect &rect, const Point &p); const Rect & center(const Rect &r, bool propagate = true); //sets pos so that r will be in the center of screen, assigns sizes of r to pos, returns new position const Rect & center(const Point &p, bool propagate = true); //moves object so that point p will be in its center const Rect & center(bool propagate = true); //centers when pos.w and pos.h are set, returns new position void fitToScreen(int borderWidth, bool propagate = true); //moves window to fit into screen void moveBy(const Point &p, bool propagate = true); void moveTo(const Point &p, bool propagate = true);//move this to new position, coordinates are absolute (0,0 is topleft screen corner) void addChild(CIntObject *child, bool adjustPosition = false); void removeChild(CIntObject *child, bool adjustPosition = false); //delChild - not needed, use normal "delete child" instead //delChildNull - not needed, use "vstd::clear_pointer(child)" instead /* * Functions that should be used only by specific GUI elements. Don't use them unless you really know why they are here */ //functions for printing text. Use CLabel where possible instead void printAtLoc(const std::string & text, int x, int y, EFonts font, SDL_Color color, SDL_Surface * dst); void printAtMiddleLoc(const std::string & text, int x, int y, EFonts font, SDL_Color color, SDL_Surface * dst); void printAtMiddleLoc(const std::string & text, const Point &p, EFonts font, SDL_Color color, SDL_Surface * dst); void printAtMiddleWBLoc(const std::string & text, int x, int y, EFonts font, int charsPerLine, SDL_Color color, SDL_Surface * dst); //image blitting. If possible use CPicture or CAnimImage instead void blitAtLoc(SDL_Surface * src, int x, int y, SDL_Surface * dst); void blitAtLoc(SDL_Surface * src, const Point &p, SDL_Surface * dst); friend class CGuiHandler; }; /// Class for binding keys to left mouse button clicks /// Classes wanting use it should have it as one of their base classes class CKeyShortcut : public virtual CIntObject { public: std::set assignedKeys; CKeyShortcut(); CKeyShortcut(int key); CKeyShortcut(std::set Keys); virtual void keyPressed(const SDL_KeyboardEvent & key) override; //call-in }; class WindowBase : public CIntObject { public: WindowBase(int used_ = 0, Point pos_ = Point()); protected: void close(); }; vcmi-0.99+git20190113.f06c8a87/client/gui/Fonts.cpp000066400000000000000000000264271342332007200210240ustar00rootroot00000000000000/* * Fonts.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Fonts.h" #include #include "SDL_Pixels.h" #include "../../lib/JsonNode.h" #include "../../lib/vcmi_endian.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/CGeneralTextHandler.h" size_t IFont::getStringWidth(const std::string & data) const { size_t width = 0; for(size_t i=0; i & data, const SDL_Color & color, const Point & pos) const { Point currPos = pos; for(const std::string & line : data) { renderTextLeft(surface, line, color, currPos); currPos.y += getLineHeight(); } } void IFont::renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const { Point currPos = pos; currPos.y -= data.size() * getLineHeight(); for(const std::string & line : data) { renderTextRight(surface, line, color, currPos); currPos.y += getLineHeight(); } } void IFont::renderTextLinesCenter(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const { Point currPos = pos; currPos.y -= data.size() * getLineHeight()/2; for(const std::string & line : data) { renderTextCenter(surface, line, color, currPos); currPos.y += getLineHeight(); } } std::array CBitmapFont::loadChars() const { std::array ret; size_t offset = 32; for (auto & elem : ret) { elem.leftOffset = read_le_u32(data.first.get() + offset); offset+=4; elem.width = read_le_u32(data.first.get() + offset); offset+=4; elem.rightOffset = read_le_u32(data.first.get() + offset); offset+=4; } for (auto & elem : ret) { int pixelOffset = read_le_u32(data.first.get() + offset); offset+=4; elem.pixels = data.first.get() + 4128 + pixelOffset; assert(pixelOffset + 4128 < data.second); } return ret; } CBitmapFont::CBitmapFont(const std::string & filename): data(CResourceHandler::get()->load(ResourceID("data/" + filename, EResType::BMP_FONT))->readAll()), chars(loadChars()), height(data.first.get()[5]) {} size_t CBitmapFont::getLineHeight() const { return height; } size_t CBitmapFont::getGlyphWidth(const char * data) const { std::string localChar = Unicode::fromUnicode(std::string(data, Unicode::getCharacterSize(data[0]))); if (localChar.size() == 1) { const BitmapChar & ch = chars[ui8(localChar[0])]; return ch.leftOffset + ch.width + ch.rightOffset; } return 0; } void CBitmapFont::renderCharacter(SDL_Surface * surface, const BitmapChar & character, const SDL_Color & color, int &posX, int &posY) const { Rect clipRect; SDL_GetClipRect(surface, &clipRect); posX += character.leftOffset; TColorPutter colorPutter = CSDL_Ext::getPutterFor(surface, 0); Uint8 bpp = surface->format->BytesPerPixel; // start of line, may differ from 0 due to end of surface or clipped surface int lineBegin = std::max(0, clipRect.y - posY); int lineEnd = std::min(height, clipRect.y + clipRect.h - posY - 1); // start end end of each row, may differ from 0 int rowBegin = std::max(0, clipRect.x - posX); int rowEnd = std::min(character.width, clipRect.x + clipRect.w - posX - 1); //for each line in symbol for(int dy = lineBegin; dy pixels; Uint8 *srcLine = character.pixels; // shift source\destination pixels to current position dstLine += (posY+dy) * surface->pitch + posX * bpp; srcLine += dy * character.width; //for each column in line for(int dx = rowBegin; dx < rowEnd; dx++) { Uint8* dstPixel = dstLine + dx*bpp; switch(srcLine[dx]) { case 1: //black "shadow" colorPutter(dstPixel, 0, 0, 0); break; case 255: //text colour colorPutter(dstPixel, color.r, color.g, color.b); break; default : break; //transparency } } } posX += character.width; posX += character.rightOffset; } void CBitmapFont::renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const { if (data.empty()) return; assert(surface); int posX = pos.x; int posY = pos.y; // Should be used to detect incorrect text parsing. Disabled right now due to some old UI code (mostly pregame and battles) //assert(data[0] != '{'); //assert(data[data.size()-1] != '}'); SDL_LockSurface(surface); for(size_t i=0; i, ui64> CTrueTypeFont::loadData(const JsonNode & config) { std::string filename = "Data/" + config["file"].String(); return CResourceHandler::get()->load(ResourceID(filename, EResType::TTF_FONT))->readAll(); } TTF_Font * CTrueTypeFont::loadFont(const JsonNode &config) { int pointSize = config["size"].Float(); if(!TTF_WasInit() && TTF_Init()==-1) throw std::runtime_error(std::string("Failed to initialize true type support: ") + TTF_GetError() + "\n"); return TTF_OpenFontRW(SDL_RWFromConstMem(data.first.get(), data.second), 1, pointSize); } int CTrueTypeFont::getFontStyle(const JsonNode &config) { const JsonVector & names = config["style"].Vector(); int ret = 0; for(const JsonNode & node : names) { if (node.String() == "bold") ret |= TTF_STYLE_BOLD; else if (node.String() == "italic") ret |= TTF_STYLE_ITALIC; } return ret; } CTrueTypeFont::CTrueTypeFont(const JsonNode & fontConfig): data(loadData(fontConfig)), font(loadFont(fontConfig), TTF_CloseFont), blended(fontConfig["blend"].Bool()) { assert(font); TTF_SetFontStyle(font.get(), getFontStyle(fontConfig)); } size_t CTrueTypeFont::getLineHeight() const { return TTF_FontHeight(font.get()); } size_t CTrueTypeFont::getGlyphWidth(const char *data) const { return getStringWidth(std::string(data, Unicode::getCharacterSize(*data))); /* int advance; TTF_GlyphMetrics(font.get(), *data, nullptr, nullptr, nullptr, nullptr, &advance); return advance; */ } size_t CTrueTypeFont::getStringWidth(const std::string & data) const { int width; TTF_SizeUTF8(font.get(), data.c_str(), &width, nullptr); return width; } void CTrueTypeFont::renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const { if (color.r != 0 && color.g != 0 && color.b != 0) // not black - add shadow { SDL_Color black = { 0, 0, 0, SDL_ALPHA_OPAQUE}; renderText(surface, data, black, Point(pos.x + 1, pos.y + 1)); } if (!data.empty()) { SDL_Surface * rendered; if (blended) rendered = TTF_RenderUTF8_Blended(font.get(), data.c_str(), color); else rendered = TTF_RenderUTF8_Solid(font.get(), data.c_str(), color); assert(rendered); Rect rect(pos.x, pos.y, rendered->w, rendered->h); SDL_BlitSurface(rendered, nullptr, surface, &rect); SDL_FreeSurface(rendered); } } size_t CBitmapHanFont::getCharacterDataOffset(size_t index) const { size_t rowSize = (size + 7) / 8; // 1 bit per pixel, rounded up size_t charSize = rowSize * size; // glyph contains "size" rows return index * charSize; } size_t CBitmapHanFont::getCharacterIndex(ui8 first, ui8 second) const { if (second > 0x7f ) second--; return (first - 0x81) * (12*16 - 2) + (second - 0x40); } void CBitmapHanFont::renderCharacter(SDL_Surface * surface, int characterIndex, const SDL_Color & color, int &posX, int &posY) const { //TODO: somewhat duplicated with CBitmapFont::renderCharacter(); Rect clipRect; SDL_GetClipRect(surface, &clipRect); TColorPutter colorPutter = CSDL_Ext::getPutterFor(surface, 0); Uint8 bpp = surface->format->BytesPerPixel; // start of line, may differ from 0 due to end of surface or clipped surface int lineBegin = std::max(0, clipRect.y - posY); int lineEnd = std::min(size, clipRect.y + clipRect.h - posY); // start end end of each row, may differ from 0 int rowBegin = std::max(0, clipRect.x - posX); int rowEnd = std::min(size, clipRect.x + clipRect.w - posX); //for each line in symbol for(int dy = lineBegin; dy pixels; Uint8 *source = data.first.get() + getCharacterDataOffset(characterIndex); // shift source\destination pixels to current position dstLine += (posY+dy) * surface->pitch + posX * bpp; source += ((size + 7) / 8) * dy; //for each column in line for(int dx = rowBegin; dx < rowEnd; dx++) { // select current bit in bitmap int bit = (source[dx / 8] << (dx % 8)) & 0x80; Uint8* dstPixel = dstLine + dx*bpp; if (bit != 0) colorPutter(dstPixel, color.r, color.g, color.b); } } posX += size + 1; } void CBitmapHanFont::renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const { int posX = pos.x; int posY = pos.y; SDL_LockSurface(surface); for(size_t i=0; irenderCharacter(surface, fallback->chars[ui8(localChar[0])], color, posX, posY); if (localChar.size() == 2) renderCharacter(surface, getCharacterIndex(localChar[0], localChar[1]), color, posX, posY); } SDL_UnlockSurface(surface); } CBitmapHanFont::CBitmapHanFont(const JsonNode &config): fallback(new CBitmapFont(config["fallback"].String())), data(CResourceHandler::get()->load(ResourceID("data/" + config["name"].String(), EResType::OTHER))->readAll()), size(config["size"].Float()) { // basic tests to make sure that fonts are OK // 1) fonts must contain 190 "sections", 126 symbols each. assert(getCharacterIndex(0xfe, 0xff) == 190*126); // 2) ensure that font size is correct - enough to fit all possible symbols assert(getCharacterDataOffset(getCharacterIndex(0xfe, 0xff)) == data.second); } size_t CBitmapHanFont::getLineHeight() const { return std::max(size + 1, fallback->getLineHeight()); } size_t CBitmapHanFont::getGlyphWidth(const char * data) const { std::string localChar = Unicode::fromUnicode(std::string(data, Unicode::getCharacterSize(data[0]))); if (localChar.size() == 1) return fallback->getGlyphWidth(data); if (localChar.size() == 2) return size + 1; return 0; } vcmi-0.99+git20190113.f06c8a87/client/gui/Fonts.h000066400000000000000000000106351342332007200204630ustar00rootroot00000000000000/* * Fonts.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once class JsonNode; struct Point; struct SDL_Surface; struct SDL_Color; typedef struct _TTF_Font TTF_Font; class CBitmapFont; class CBitmapHanFont; class IFont { protected: /// Internal function to render font, see renderTextLeft virtual void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const = 0; public: virtual ~IFont() {} /// Returns height of font virtual size_t getLineHeight() const = 0; /// Returns width, in pixels of a character glyph. Pointer must contain at least characterSize valid bytes virtual size_t getGlyphWidth(const char * data) const = 0; /// Return width of the string virtual size_t getStringWidth(const std::string & data) const; /** * @param surface - destination to print text on * @param data - string to print * @param color - font color * @param pos - position of rendered font */ /// pos = topleft corner of the text void renderTextLeft(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const; /// pos = center of the text void renderTextRight(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const; /// pos = bottomright corner of the text void renderTextCenter(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const; /// pos = topleft corner of the text void renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const; /// pos = center of the text void renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const; /// pos = bottomright corner of the text void renderTextLinesCenter(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const; }; class CBitmapFont : public IFont { static const size_t totalChars = 256; struct BitmapChar { si32 leftOffset; ui32 width; si32 rightOffset; ui8 *pixels; // pixels of this character, part of BitmapFont::data }; const std::pair, ui64> data; const std::array chars; const ui8 height; std::array loadChars() const; void renderCharacter(SDL_Surface * surface, const BitmapChar & character, const SDL_Color & color, int &posX, int &posY) const; void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const override; public: CBitmapFont(const std::string & filename); size_t getLineHeight() const override; size_t getGlyphWidth(const char * data) const override; friend class CBitmapHanFont; }; /// supports multi-byte characters for such languages like Chinese class CBitmapHanFont : public IFont { std::unique_ptr fallback; // data, directly copied from file const std::pair, ui64> data; // size of the font. Not available in file but needed for proper rendering const size_t size; size_t getCharacterDataOffset(size_t index) const; size_t getCharacterIndex(ui8 first, ui8 second) const; void renderCharacter(SDL_Surface * surface, int characterIndex, const SDL_Color & color, int &posX, int &posY) const; void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const override; public: CBitmapHanFont(const JsonNode & config); size_t getLineHeight() const override; size_t getGlyphWidth(const char * data) const override; }; class CTrueTypeFont : public IFont { const std::pair, ui64> data; const std::unique_ptr font; const bool blended; std::pair, ui64> loadData(const JsonNode & config); TTF_Font * loadFont(const JsonNode & config); int getFontStyle(const JsonNode & config); void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const override; public: CTrueTypeFont(const JsonNode & fontConfig); size_t getLineHeight() const override; size_t getGlyphWidth(const char * data) const override; size_t getStringWidth(const std::string & data) const override; }; vcmi-0.99+git20190113.f06c8a87/client/gui/Geometries.cpp000066400000000000000000000014071342332007200220250ustar00rootroot00000000000000/* * Geometries.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Geometries.h" #include "../CMT.h" #include Point::Point(const SDL_MouseMotionEvent &a) :x(a.x),y(a.y) {} Rect Rect::createCentered( int w, int h ) { return Rect(screen->w/2 - w/2, screen->h/2 - h/2, w, h); } Rect Rect::around(const Rect &r, int width) /*creates rect around another */ { return Rect(r.x - width, r.y - width, r.w + width * 2, r.h + width * 2); } Rect Rect::centerIn(const Rect &r) { return Rect(r.x + (r.w - w) / 2, r.y + (r.h - h) / 2, w, h); } vcmi-0.99+git20190113.f06c8a87/client/gui/Geometries.h000066400000000000000000000121561342332007200214750ustar00rootroot00000000000000/* * Geometries.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include #include "../../lib/int3.h" struct SDL_MouseMotionEvent; // A point with x/y coordinate, used mostly for graphic rendering struct Point { int x, y; //constructors Point() { x = y = 0; }; Point(int X, int Y) :x(X),y(Y) {}; Point(const int3 &a) :x(a.x),y(a.y) {} Point(const SDL_MouseMotionEvent &a); template Point operator+(const T &b) const { return Point(x+b.x,y+b.y); } template Point operator/(const T &div) const { return Point(x/div, y/div); } template Point operator*(const T &mul) const { return Point(x*mul, y*mul); } template Point& operator+=(const T &b) { x += b.x; y += b.y; return *this; } template Point operator-(const T &b) const { return Point(x - b.x, y - b.y); } template Point& operator-=(const T &b) { x -= b.x; y -= b.y; return *this; } bool operator<(const Point &b) const //product order { return x < b.x && y < b.y; } template Point& operator=(const T &t) { x = t.x; y = t.y; return *this; } template bool operator==(const T &t) const { return x == t.x && y == t.y; } template bool operator!=(const T &t) const { return !(*this == t); } }; /// Rectangle class, which have a position and a size struct Rect : public SDL_Rect { Rect()//default c-tor { x = y = w = h = -1; } Rect(int X, int Y, int W, int H) { x = X; y = Y; w = W; h = H; } Rect(const Point & position, const Point & size) { x = position.x; y = position.y; w = size.x; h = size.y; } Rect(const SDL_Rect & r) { x = r.x; y = r.y; w = r.w; h = r.h; } explicit Rect(const SDL_Surface * const &surf) { x = y = 0; w = surf->w; h = surf->h; } Rect centerIn(const Rect &r); static Rect createCentered(int w, int h); static Rect around(const Rect &r, int width = 1); //creates rect around another bool isIn(int qx, int qy) const //determines if given point lies inside rect { if (qx > x && qxy && qy Rect operator-(const T &t) { return Rect(x - t.x, y - t.y, w, h); } Rect operator&(const Rect &p) const //rect intersection { bool intersect = true; if(p.topLeft().y < y && p.bottomLeft().y < y) //rect p is above *this { intersect = false; } else if(p.topLeft().y > y+h && p.bottomLeft().y > y+h) //rect p is below *this { intersect = false; } else if(p.topLeft().x > x+w && p.topRight().x > x+w) //rect p is on the right hand side of this { intersect = false; } else if(p.topLeft().x < x && p.topRight().x < x) //rect p is on the left hand side of this { intersect = false; } if(intersect) { Rect ret; ret.x = std::max(this->x, p.x); ret.y = std::max(this->y, p.y); Point bR; //bottomRight point of returned rect bR.x = std::min(this->w+this->x, p.w+p.x); bR.y = std::min(this->h+this->y, p.h+p.y); ret.w = bR.x - ret.x; ret.h = bR.y - ret.y; return ret; } else { return Rect(); } } Rect operator|(const Rect &p) const //union of two rects { Rect ret; ret.x = std::min(p.x, this->x); ret.y = std::min(p.y, this->y); int x2 = std::max(p.x+p.w, this->x+this->w); int y2 = std::max(p.y+p.h, this->y+this->h); ret.w = x2 -ret.x; ret.h = y2 -ret.y; return ret; } }; vcmi-0.99+git20190113.f06c8a87/client/gui/SDL_Compat.h000066400000000000000000000006571342332007200213220ustar00rootroot00000000000000/* * SDL_Compat.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include #if (SDL_MAJOR_VERSION == 2) #include typedef int SDLX_Coord; typedef int SDLX_Size; #else #error "unknown or unsupported SDL version" #endif vcmi-0.99+git20190113.f06c8a87/client/gui/SDL_Extensions.cpp000066400000000000000000000544241342332007200225720ustar00rootroot00000000000000/* * SDL_Extensions.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "SDL_Extensions.h" #include "SDL_Pixels.h" #include "../CGameInfo.h" #include "../CMessage.h" #include "../Graphics.h" #include "../CMT.h" const SDL_Color Colors::YELLOW = { 229, 215, 123, 0 }; const SDL_Color Colors::WHITE = { 255, 243, 222, 0 }; const SDL_Color Colors::METALLIC_GOLD = { 173, 142, 66, 0 }; const SDL_Color Colors::GREEN = { 0, 255, 0, 0 }; const SDL_Color Colors::ORANGE = { 232, 184, 32, 0 }; const SDL_Color Colors::BRIGHT_YELLOW = { 242, 226, 110, 0 }; const SDL_Color Colors::DEFAULT_KEY_COLOR = {0, 255, 255, 0}; void SDL_UpdateRect(SDL_Surface *surface, int x, int y, int w, int h) { Rect rect(x,y,w,h); if(0 !=SDL_UpdateTexture(screenTexture, &rect, surface->pixels, surface->pitch)) logGlobal->error("%sSDL_UpdateTexture %s", __FUNCTION__, SDL_GetError()); SDL_RenderClear(mainRenderer); if(0 != SDL_RenderCopy(mainRenderer, screenTexture, NULL, NULL)) logGlobal->error("%sSDL_RenderCopy %s", __FUNCTION__, SDL_GetError()); SDL_RenderPresent(mainRenderer); } SDL_Surface * CSDL_Ext::newSurface(int w, int h, SDL_Surface * mod) //creates new surface, with flags/format same as in surface given { SDL_Surface * ret = SDL_CreateRGBSurface(0,w,h,mod->format->BitsPerPixel,mod->format->Rmask,mod->format->Gmask,mod->format->Bmask,mod->format->Amask); if (mod->format->palette) { assert(ret->format->palette); assert(ret->format->palette->ncolors == mod->format->palette->ncolors); memcpy(ret->format->palette->colors, mod->format->palette->colors, mod->format->palette->ncolors * sizeof(SDL_Color)); } return ret; } SDL_Surface * CSDL_Ext::copySurface(SDL_Surface * mod) //returns copy of given surface { //return SDL_DisplayFormat(mod); return SDL_ConvertSurface(mod, mod->format, mod->flags); } template SDL_Surface * CSDL_Ext::createSurfaceWithBpp(int width, int height) { Uint32 rMask = 0, gMask = 0, bMask = 0, aMask = 0; Channels::px::r.set((Uint8*)&rMask, 255); Channels::px::g.set((Uint8*)&gMask, 255); Channels::px::b.set((Uint8*)&bMask, 255); Channels::px::a.set((Uint8*)&aMask, 255); return SDL_CreateRGBSurface(0, width, height, bpp * 8, rMask, gMask, bMask, aMask); } bool isItIn(const SDL_Rect * rect, int x, int y) { return (x>rect->x && xx+rect->w) && (y>rect->y && yy+rect->h); } bool isItInOrLowerBounds(const SDL_Rect * rect, int x, int y) { return (x >= rect->x && x < rect->x + rect->w) && (y >= rect->y && y < rect->y + rect->h); } void blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst) { if(!dst) dst = screen; SDL_Rect pom = genRect(src->h,src->w,x,y); CSDL_Ext::blitSurface(src,nullptr,dst,&pom); } void blitAt(SDL_Surface * src, const SDL_Rect & pos, SDL_Surface * dst) { if (src) blitAt(src,pos.x,pos.y,dst); } // Vertical flip SDL_Surface * CSDL_Ext::verticalFlip(SDL_Surface * toRot) { SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); SDL_LockSurface(ret); SDL_LockSurface(toRot); const int bpp = ret->format->BytesPerPixel; char * src = reinterpret_cast(toRot->pixels); char * dst = reinterpret_cast(ret->pixels); for(int i=0; ih; i++) { //FIXME: optimization bugged // if (bpp == 1) // { // // much faster for 8-bit surfaces (majority of our data) // std::reverse_copy(src, src + toRot->pitch, dst); // } // else // { char * srcPxl = src; char * dstPxl = dst + ret->w * bpp; for(int j=0; jw; j++) { dstPxl -= bpp; std::copy(srcPxl, srcPxl + bpp, dstPxl); srcPxl += bpp; } // } src += toRot->pitch; dst += ret->pitch; } SDL_UnlockSurface(ret); SDL_UnlockSurface(toRot); return ret; } // Horizontal flip SDL_Surface * CSDL_Ext::horizontalFlip(SDL_Surface * toRot) { SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); SDL_LockSurface(ret); SDL_LockSurface(toRot); char * src = reinterpret_cast(toRot->pixels); char * dst = reinterpret_cast(ret->pixels) + ret->h * ret->pitch; for(int i=0; ih; i++) { dst -= ret->pitch; std::copy(src, src + toRot->pitch, dst); src += toRot->pitch; } SDL_UnlockSurface(ret); SDL_UnlockSurface(toRot); return ret; } Uint32 CSDL_Ext::SDL_GetPixel(SDL_Surface *surface, const int & x, const int & y, bool colorByte) { int bpp = surface->format->BytesPerPixel; /* Here p is the address to the pixel we want to retrieve */ Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp; switch(bpp) { case 1: if(colorByte) return colorToUint32(surface->format->palette->colors+(*p)); else return *p; case 2: return *(Uint16 *)p; case 3: return p[0] | p[1] << 8 | p[2] << 16; case 4: return *(Uint32 *)p; default: return 0; // shouldn't happen, but avoids warnings } } void CSDL_Ext::alphaTransform(SDL_Surface *src) { assert(src->format->BitsPerPixel == 8); SDL_Color colors[] = { { 0, 0, 0, 0}, { 0, 0, 0, 32}, { 0, 0, 0, 64}, { 0, 0, 0, 128}, { 0, 0, 0, 128} }; for (size_t i=0; i< ARRAY_COUNT(colors); i++ ) { SDL_Color & palColor = src->format->palette->colors[i]; palColor = colors[i]; } SDL_SetColorKey(src, SDL_TRUE, 0); } template int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect) { /* Make sure the surfaces aren't locked */ if ( ! src || ! dst ) { SDL_SetError("SDL_UpperBlit: passed a nullptr surface"); return -1; } if ( src->locked || dst->locked ) { SDL_SetError("Surfaces must not be locked during blit"); return -1; } if (src->format->BytesPerPixel==1 && (bpp==3 || bpp==4 || bpp==2)) //everything's ok { SDL_Rect fulldst; int srcx, srcy, w, h; /* If the destination rectangle is nullptr, use the entire dest surface */ if ( dstRect == nullptr ) { fulldst.x = fulldst.y = 0; dstRect = &fulldst; } /* clip the source rectangle to the source surface */ if(srcRect) { int maxw, maxh; srcx = srcRect->x; w = srcRect->w; if(srcx < 0) { w += srcx; dstRect->x -= srcx; srcx = 0; } maxw = src->w - srcx; if(maxw < w) w = maxw; srcy = srcRect->y; h = srcRect->h; if(srcy < 0) { h += srcy; dstRect->y -= srcy; srcy = 0; } maxh = src->h - srcy; if(maxh < h) h = maxh; } else { srcx = srcy = 0; w = src->w; h = src->h; } /* clip the destination rectangle against the clip rectangle */ { SDL_Rect *clip = &dst->clip_rect; int dx, dy; dx = clip->x - dstRect->x; if(dx > 0) { w -= dx; dstRect->x += dx; srcx += dx; } dx = dstRect->x + w - clip->x - clip->w; if(dx > 0) w -= dx; dy = clip->y - dstRect->y; if(dy > 0) { h -= dy; dstRect->y += dy; srcy += dy; } dy = dstRect->y + h - clip->y - clip->h; if(dy > 0) h -= dy; } if(w > 0 && h > 0) { dstRect->w = w; dstRect->h = h; if(SDL_LockSurface(dst)) return -1; //if we cannot lock the surface const SDL_Color *colors = src->format->palette->colors; Uint8 *colory = (Uint8*)src->pixels + srcy*src->pitch + srcx; Uint8 *py = (Uint8*)dst->pixels + dstRect->y*dst->pitch + dstRect->x*bpp; for(int y=h; y; y--, colory+=src->pitch, py+=dst->pitch) { Uint8 *color = colory; Uint8 *p = py; for(int x = w; x; x--) { const SDL_Color &tbc = colors[*color++]; //color to blit ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, tbc.a); } } SDL_UnlockSurface(dst); } } return 0; } int CSDL_Ext::blit8bppAlphaTo24bpp(const SDL_Surface * src, const SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect) { switch(dst->format->BytesPerPixel) { case 2: return blit8bppAlphaTo24bppT<2>(src, srcRect, dst, dstRect); case 3: return blit8bppAlphaTo24bppT<3>(src, srcRect, dst, dstRect); case 4: return blit8bppAlphaTo24bppT<4>(src, srcRect, dst, dstRect); default: logGlobal->error("%d bpp is not supported!", (int)dst->format->BitsPerPixel); return -1; } } Uint32 CSDL_Ext::colorToUint32(const SDL_Color * color) { Uint32 ret = 0; ret+=color->a; ret<<=8; //*=256 ret+=color->b; ret<<=8; //*=256 ret+=color->g; ret<<=8; //*=256 ret+=color->r; return ret; } void CSDL_Ext::update(SDL_Surface * what) { if(!what) return; if(0 !=SDL_UpdateTexture(screenTexture, nullptr, what->pixels, what->pitch)) logGlobal->error("%s SDL_UpdateTexture %s", __FUNCTION__, SDL_GetError()); } void CSDL_Ext::drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const int3 &color) { for(int i = 0; i < w; i++) { SDL_PutPixelWithoutRefreshIfInSurf(sur,x+i,y,color.x,color.y,color.z); SDL_PutPixelWithoutRefreshIfInSurf(sur,x+i,y+h-1,color.x,color.y,color.z); } for(int i = 0; i < h; i++) { SDL_PutPixelWithoutRefreshIfInSurf(sur,x,y+i,color.x,color.y,color.z); SDL_PutPixelWithoutRefreshIfInSurf(sur,x+w-1,y+i,color.x,color.y,color.z); } } void CSDL_Ext::drawBorder( SDL_Surface * sur, const SDL_Rect &r, const int3 &color ) { drawBorder(sur, r.x, r.y, r.w, r.h, color); } void CSDL_Ext::drawDashedBorder(SDL_Surface * sur, const Rect &r, const int3 &color) { const int y1 = r.y, y2 = r.y + r.h-1; for (int i=0; iformat->BitsPerPixel==8) { SDL_Color *color = (player == PlayerColor::NEUTRAL ? graphics->neutralColor : &graphics->playerColors[player.getNum()]); SDL_SetColors(sur, color, 5, 1); } else logGlobal->warn("Warning, setPlayerColor called on not 8bpp surface!"); } TColorPutter CSDL_Ext::getPutterFor(SDL_Surface * const &dest, int incrementing) { #define CASE_BPP(BytesPerPixel) \ case BytesPerPixel: \ if(incrementing > 0) \ return ColorPutter::PutColor; \ else if(incrementing == 0) \ return ColorPutter::PutColor; \ else \ return ColorPutter::PutColor;\ break; switch(dest->format->BytesPerPixel) { CASE_BPP(2) CASE_BPP(3) CASE_BPP(4) default: logGlobal->error("%d bpp is not supported!", (int)dest->format->BitsPerPixel); return nullptr; } } TColorPutterAlpha CSDL_Ext::getPutterAlphaFor(SDL_Surface * const &dest, int incrementing) { switch(dest->format->BytesPerPixel) { CASE_BPP(2) CASE_BPP(3) CASE_BPP(4) default: logGlobal->error("%d bpp is not supported!", (int)dest->format->BitsPerPixel); return nullptr; } #undef CASE_BPP } Uint8 * CSDL_Ext::getPxPtr(const SDL_Surface * const &srf, const int x, const int y) { return (Uint8 *)srf->pixels + y * srf->pitch + x * srf->format->BytesPerPixel; } std::string CSDL_Ext::processStr(std::string str, std::vector & tor) { for (size_t i=0; (i= srf->w || y >= srf->h) return true; SDL_Color color; SDL_GetRGBA(SDL_GetPixel(srf, x, y), srf->format, &color.r, &color.g, &color.b, &color.a); // color is considered transparent here if // a) image has aplha: less than 50% transparency // b) no alpha: color is cyan if (srf->format->Amask) return color.a < 128; // almost transparent else return (color.r == 0 && color.g == 255 && color.b == 255); } void CSDL_Ext::VflipSurf(SDL_Surface * surf) { char buf[4]; //buffer int bpp = surf->format->BytesPerPixel; for (int y=0; yh; ++y) { char * base = (char*)surf->pixels + y * surf->pitch; for (int x=0; xw/2; ++x) { memcpy(buf, base + x * bpp, bpp); memcpy(base + x * bpp, base + (surf->w - x - 1) * bpp, bpp); memcpy(base + (surf->w - x - 1) * bpp, buf, bpp); } } } void CSDL_Ext::SDL_PutPixelWithoutRefresh(SDL_Surface *ekran, const int & x, const int & y, const Uint8 & R, const Uint8 & G, const Uint8 & B, Uint8 A) { Uint8 *p = getPxPtr(ekran, x, y); getPutterFor(ekran, false)(p, R, G, B); switch(ekran->format->BytesPerPixel) { case 2: Channels::px<2>::a.set(p, A); break; case 3: Channels::px<3>::a.set(p, A); break; case 4: Channels::px<4>::a.set(p, A); break; } } void CSDL_Ext::SDL_PutPixelWithoutRefreshIfInSurf(SDL_Surface *ekran, const int & x, const int & y, const Uint8 & R, const Uint8 & G, const Uint8 & B, Uint8 A) { const SDL_Rect & rect = ekran->clip_rect; if(x >= rect.x && x < rect.w + rect.x && y >= rect.y && y < rect.h + rect.y) SDL_PutPixelWithoutRefresh(ekran, x, y, R, G, B, A); } template void CSDL_Ext::applyEffectBpp( SDL_Surface * surf, const SDL_Rect * rect, int mode ) { switch(mode) { case 0: //sepia { const int sepiaDepth = 20; const int sepiaIntensity = 30; for(int xp = rect->x; xp < rect->x + rect->w; ++xp) { for(int yp = rect->y; yp < rect->y + rect->h; ++yp) { Uint8 * pixel = (ui8*)surf->pixels + yp * surf->pitch + xp * surf->format->BytesPerPixel; int r = Channels::px::r.get(pixel); int g = Channels::px::g.get(pixel); int b = Channels::px::b.get(pixel); int gray = 0.299 * r + 0.587 * g + 0.114 *b; r = g = b = gray; r = r + (sepiaDepth * 2); g = g + sepiaDepth; if (r>255) r=255; if (g>255) g=255; if (b>255) b=255; // Darken blue color to increase sepia effect b -= sepiaIntensity; // normalize if out of bounds if (b<0) b=0; Channels::px::r.set(pixel, r); Channels::px::g.set(pixel, g); Channels::px::b.set(pixel, b); } } } break; case 1: //grayscale { for(int xp = rect->x; xp < rect->x + rect->w; ++xp) { for(int yp = rect->y; yp < rect->y + rect->h; ++yp) { Uint8 * pixel = (ui8*)surf->pixels + yp * surf->pitch + xp * surf->format->BytesPerPixel; int r = Channels::px::r.get(pixel); int g = Channels::px::g.get(pixel); int b = Channels::px::b.get(pixel); int gray = 0.299 * r + 0.587 * g + 0.114 *b; vstd::amax(gray, 255); Channels::px::r.set(pixel, gray); Channels::px::g.set(pixel, gray); Channels::px::b.set(pixel, gray); } } } break; default: throw std::runtime_error("Unsupported effect!"); } } void CSDL_Ext::applyEffect( SDL_Surface * surf, const SDL_Rect * rect, int mode ) { switch(surf->format->BytesPerPixel) { case 2: applyEffectBpp<2>(surf, rect, mode); break; case 3: applyEffectBpp<3>(surf, rect, mode); break; case 4: applyEffectBpp<4>(surf, rect, mode); break; } } template void scaleSurfaceFastInternal(SDL_Surface *surf, SDL_Surface *ret) { const float factorX = float(surf->w) / float(ret->w), factorY = float(surf->h) / float(ret->h); for(int y = 0; y < ret->h; y++) { for(int x = 0; x < ret->w; x++) { //coordinates we want to calculate int origX = floor(factorX * x), origY = floor(factorY * y); // Get pointers to source pixels Uint8 *srcPtr = (Uint8*)surf->pixels + origY * surf->pitch + origX * bpp; Uint8 *destPtr = (Uint8*)ret->pixels + y * ret->pitch + x * bpp; memcpy(destPtr, srcPtr, bpp); } } } SDL_Surface * CSDL_Ext::scaleSurfaceFast(SDL_Surface *surf, int width, int height) { if (!surf || !width || !height) return nullptr; //Same size? return copy - this should more be faster if (width == surf->w && height == surf->h) return copySurface(surf); SDL_Surface *ret = newSurface(width, height, surf); switch(surf->format->BytesPerPixel) { case 1: scaleSurfaceFastInternal<1>(surf, ret); break; case 2: scaleSurfaceFastInternal<2>(surf, ret); break; case 3: scaleSurfaceFastInternal<3>(surf, ret); break; case 4: scaleSurfaceFastInternal<4>(surf, ret); break; } return ret; } template void scaleSurfaceInternal(SDL_Surface *surf, SDL_Surface *ret) { const float factorX = float(surf->w - 1) / float(ret->w), factorY = float(surf->h - 1) / float(ret->h); for(int y = 0; y < ret->h; y++) { for(int x = 0; x < ret->w; x++) { //coordinates we want to interpolate float origX = factorX * x, origY = factorY * y; float x1 = floor(origX), x2 = floor(origX+1), y1 = floor(origY), y2 = floor(origY+1); //assert( x1 >= 0 && y1 >= 0 && x2 < surf->w && y2 < surf->h);//All pixels are in range // Calculate weights of each source pixel float w11 = ((origX - x1) * (origY - y1)); float w12 = ((origX - x1) * (y2 - origY)); float w21 = ((x2 - origX) * (origY - y1)); float w22 = ((x2 - origX) * (y2 - origY)); //assert( w11 + w12 + w21 + w22 > 0.99 && w11 + w12 + w21 + w22 < 1.01);//total weight is ~1.0 // Get pointers to source pixels Uint8 *p11 = (Uint8*)surf->pixels + int(y1) * surf->pitch + int(x1) * bpp; Uint8 *p12 = p11 + bpp; Uint8 *p21 = p11 + surf->pitch; Uint8 *p22 = p21 + bpp; // Calculate resulting channels #define PX(X, PTR) Channels::px::X.get(PTR) int resR = PX(r, p11) * w11 + PX(r, p12) * w12 + PX(r, p21) * w21 + PX(r, p22) * w22; int resG = PX(g, p11) * w11 + PX(g, p12) * w12 + PX(g, p21) * w21 + PX(g, p22) * w22; int resB = PX(b, p11) * w11 + PX(b, p12) * w12 + PX(b, p21) * w21 + PX(b, p22) * w22; int resA = PX(a, p11) * w11 + PX(a, p12) * w12 + PX(a, p21) * w21 + PX(a, p22) * w22; //assert(resR < 256 && resG < 256 && resB < 256 && resA < 256); #undef PX Uint8 *dest = (Uint8*)ret->pixels + y * ret->pitch + x * bpp; Channels::px::r.set(dest, resR); Channels::px::g.set(dest, resG); Channels::px::b.set(dest, resB); Channels::px::a.set(dest, resA); } } } // scaling via bilinear interpolation algorithm. // NOTE: best results are for scaling in range 50%...200%. // And upscaling looks awful right now - should be fixed somehow SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface *surf, int width, int height) { if (!surf || !width || !height) return nullptr; if (surf->format->palette) return scaleSurfaceFast(surf, width, height); //Same size? return copy - this should more be faster if (width == surf->w && height == surf->h) return copySurface(surf); SDL_Surface *ret = newSurface(width, height, surf); switch(surf->format->BytesPerPixel) { case 2: scaleSurfaceInternal<2>(surf, ret); break; case 3: scaleSurfaceInternal<3>(surf, ret); break; case 4: scaleSurfaceInternal<4>(surf, ret); break; } return ret; } void CSDL_Ext::blitSurface( SDL_Surface * src, const SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect ) { if (dst != screen) { SDL_UpperBlit(src, srcRect, dst, dstRect); } else { SDL_Rect betterDst; if (dstRect) { betterDst = *dstRect; } else { betterDst = Rect(0, 0, dst->w, dst->h); } SDL_UpperBlit(src, srcRect, dst, &betterDst); } } void CSDL_Ext::fillRect( SDL_Surface *dst, SDL_Rect *dstrect, Uint32 color ) { SDL_Rect newRect; if (dstrect) { newRect = *dstrect; } else { newRect = Rect(0, 0, dst->w, dst->h); } SDL_FillRect(dst, &newRect, color); } void CSDL_Ext::fillRectBlack( SDL_Surface *dst, SDL_Rect *dstrect) { const Uint32 black = SDL_MapRGB(dst->format,0,0,0); fillRect(dst,dstrect,black); } void CSDL_Ext::fillTexture(SDL_Surface *dst, SDL_Surface * src) { SDL_Rect srcRect; SDL_Rect dstRect; SDL_GetClipRect(src, &srcRect); SDL_GetClipRect(dst, &dstRect); for (int y=dstRect.y; y < dstRect.y + dstRect.h; y+=srcRect.h) { for (int x=dstRect.x; x < dstRect.x + dstRect.w; x+=srcRect.w) { int xLeft = std::min(srcRect.w, dstRect.x + dstRect.w - x); int yLeft = std::min(srcRect.h, dstRect.y + dstRect.h - y); Rect currentDest(x, y, xLeft, yLeft); SDL_BlitSurface(src, &srcRect, dst, ¤tDest); } } } SDL_Color CSDL_Ext::makeColor(ui8 r, ui8 g, ui8 b, ui8 a) { SDL_Color ret = {r, g, b, a}; return ret; } void CSDL_Ext::startTextInput(SDL_Rect * where) { if (SDL_IsTextInputActive() == SDL_FALSE) { SDL_StartTextInput(); } SDL_SetTextInputRect(where); } void CSDL_Ext::stopTextInput() { if (SDL_IsTextInputActive() == SDL_TRUE) { SDL_StopTextInput(); } } STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) { return SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); } void CSDL_Ext::setColorKey(SDL_Surface * surface, SDL_Color color) { uint32_t key = mapColor(surface,color); SDL_SetColorKey(surface, SDL_TRUE, key); } void CSDL_Ext::setDefaultColorKey(SDL_Surface * surface) { setColorKey(surface, Colors::DEFAULT_KEY_COLOR); } void CSDL_Ext::setDefaultColorKeyPresize(SDL_Surface * surface) { uint32_t key = mapColor(surface,Colors::DEFAULT_KEY_COLOR); auto & color = surface->format->palette->colors[key]; // set color key only if exactly such color was found if (color.r == Colors::DEFAULT_KEY_COLOR.r && color.g == Colors::DEFAULT_KEY_COLOR.g && color.b == Colors::DEFAULT_KEY_COLOR.b) SDL_SetColorKey(surface, SDL_TRUE, key); } template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<2>(int, int); template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<3>(int, int); template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<4>(int, int); vcmi-0.99+git20190113.f06c8a87/client/gui/SDL_Extensions.h000066400000000000000000000214001342332007200222230ustar00rootroot00000000000000/* * SDL_Extensions.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include #include #include #include #include "../../lib/int3.h" #include "Geometries.h" #include "../../lib/GameConstants.h" extern SDL_Window * mainWindow; extern SDL_Renderer * mainRenderer; extern SDL_Texture * screenTexture; inline void SDL_SetColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors) { SDL_SetPaletteColors(surface->format->palette,colors,firstcolor,ncolors); } inline void SDL_WarpMouse(int x, int y) { SDL_WarpMouseInWindow(mainWindow,x,y); } void SDL_UpdateRect(SDL_Surface *surface, int x, int y, int w, int h); inline bool isCtrlKeyDown() { return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RCTRL]; } inline bool isAltKeyDown() { return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LALT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RALT]; } inline bool isShiftKeyDown() { return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LSHIFT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RSHIFT]; } namespace CSDL_Ext { //todo: should this better be assignment operator? STRONG_INLINE void colorAssign(SDL_Color & dest, const SDL_Color & source) { dest.r = source.r; dest.g = source.g; dest.b = source.b; dest.a = source.a; } inline void setAlpha(SDL_Surface * bg, int value) { SDL_SetSurfaceAlphaMod(bg, value); } } struct Rect; extern SDL_Surface * screen, *screen2, *screenBuf; void blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst=screen); void blitAt(SDL_Surface * src, const SDL_Rect & pos, SDL_Surface * dst=screen); bool isItIn(const SDL_Rect * rect, int x, int y); bool isItInOrLowerBounds(const SDL_Rect * rect, int x, int y); /** * The colors class defines color constants of type SDL_Color. */ class Colors { public: /** the h3 yellow color, typically used for headlines */ static const SDL_Color YELLOW; /** the standard h3 white color */ static const SDL_Color WHITE; /** the metallic gold color used mostly as a border around buttons */ static const SDL_Color METALLIC_GOLD; /** green color used for in-game console */ static const SDL_Color GREEN; /** the h3 orange color, used for blocked buttons */ static const SDL_Color ORANGE; /** the h3 bright yellow color, used for selection border */ static const SDL_Color BRIGHT_YELLOW; /** default key color for all 8 & 24 bit graphics */ static const SDL_Color DEFAULT_KEY_COLOR; }; //MSVC gives an error when calling abs with ui64 -> we add template that will match calls with unsigned arg and return it template typename boost::enable_if_c::type, T>::type abs(T arg) { return arg; } template std::string makeNumberShort(IntType number, IntType maxLength = 3) //the output is a string containing at most 5 characters [4 if positive] (eg. intead 10000 it gives 10k) { IntType max = pow(10, maxLength); if (std::abs(number) < max) return boost::lexical_cast(number); std::string symbols = " kMGTPE"; auto iter = symbols.begin(); while (number >= max) { number /= 1000; iter++; assert(iter != symbols.end());//should be enough even for int64 } return boost::lexical_cast(number) + *iter; } typedef void (*TColorPutter)(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B); typedef void (*TColorPutterAlpha)(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B, const Uint8 & A); inline SDL_Rect genRect(const int & hh, const int & ww, const int & xx, const int & yy) { SDL_Rect ret; ret.h=hh; ret.w=ww; ret.x=xx; ret.y=yy; return ret; } template struct ColorPutter { static STRONG_INLINE void PutColor(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B); static STRONG_INLINE void PutColor(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B, const Uint8 & A); static STRONG_INLINE void PutColorAlphaSwitch(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B, const Uint8 & A); static STRONG_INLINE void PutColor(Uint8 *&ptr, const SDL_Color & Color); static STRONG_INLINE void PutColorAlpha(Uint8 *&ptr, const SDL_Color & Color); static STRONG_INLINE void PutColorRow(Uint8 *&ptr, const SDL_Color & Color, size_t count); }; typedef void (*BlitterWithRotationVal)(SDL_Surface *src,SDL_Rect srcRect, SDL_Surface * dst, SDL_Rect dstRect, ui8 rotation); namespace CSDL_Ext { /// helper that will safely set and un-set ClipRect for SDL_Surface class CClipRectGuard { SDL_Surface * surf; SDL_Rect oldRect; public: CClipRectGuard(SDL_Surface * surface, const SDL_Rect & rect): surf(surface) { SDL_GetClipRect(surf, &oldRect); SDL_SetClipRect(surf, &rect); } ~CClipRectGuard() { SDL_SetClipRect(surf, &oldRect); } }; void blitSurface(SDL_Surface * src, const SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect); void fillRect(SDL_Surface *dst, SDL_Rect *dstrect, Uint32 color); void fillRectBlack(SDL_Surface * dst, SDL_Rect * dstrect); //fill dest image with source texture. void fillTexture(SDL_Surface *dst, SDL_Surface * sourceTexture); void SDL_PutPixelWithoutRefresh(SDL_Surface *ekran, const int & x, const int & y, const Uint8 & R, const Uint8 & G, const Uint8 & B, Uint8 A = 255); void SDL_PutPixelWithoutRefreshIfInSurf(SDL_Surface *ekran, const int & x, const int & y, const Uint8 & R, const Uint8 & G, const Uint8 & B, Uint8 A = 255); SDL_Surface * verticalFlip(SDL_Surface * toRot); //vertical flip SDL_Surface * horizontalFlip(SDL_Surface * toRot); //horizontal flip Uint32 SDL_GetPixel(SDL_Surface *surface, const int & x, const int & y, bool colorByte = false); void alphaTransform(SDL_Surface * src); //adds transparency and shadows (partial handling only; see examples of using for details) bool isTransparent(SDL_Surface * srf, int x, int y); //checks if surface is transparent at given position Uint8 *getPxPtr(const SDL_Surface * const &srf, const int x, const int y); TColorPutter getPutterFor(SDL_Surface * const &dest, int incrementing); //incrementing: -1, 0, 1 TColorPutterAlpha getPutterAlphaFor(SDL_Surface * const &dest, int incrementing); //incrementing: -1, 0, 1 template int blit8bppAlphaTo24bppT(const SDL_Surface * src, const SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect); //blits 8 bpp surface with alpha channel to 24 bpp surface int blit8bppAlphaTo24bpp(const SDL_Surface * src, const SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect); //blits 8 bpp surface with alpha channel to 24 bpp surface Uint32 colorToUint32(const SDL_Color * color); //little endian only SDL_Color makeColor(ui8 r, ui8 g, ui8 b, ui8 a); void update(SDL_Surface * what = screen); //updates whole surface (default - main screen) void drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const int3 &color); void drawBorder(SDL_Surface * sur, const SDL_Rect &r, const int3 &color); void drawDashedBorder(SDL_Surface * sur, const Rect &r, const int3 &color); void setPlayerColor(SDL_Surface * sur, PlayerColor player); //sets correct color of flags; -1 for neutral std::string processStr(std::string str, std::vector & tor); //replaces %s in string SDL_Surface * newSurface(int w, int h, SDL_Surface * mod=screen); //creates new surface, with flags/format same as in surface given SDL_Surface * copySurface(SDL_Surface * mod); //returns copy of given surface template SDL_Surface * createSurfaceWithBpp(int width, int height); //create surface with give bits per pixels value void VflipSurf(SDL_Surface * surf); //fluipis given surface by vertical axis //scale surface to required size. //nearest neighbour algorithm SDL_Surface * scaleSurfaceFast(SDL_Surface *surf, int width, int height); // bilinear filtering. Uses fallback to scaleSurfaceFast in case of indexed surfaces SDL_Surface * scaleSurface(SDL_Surface *surf, int width, int height); template void applyEffectBpp( SDL_Surface * surf, const SDL_Rect * rect, int mode ); void applyEffect(SDL_Surface * surf, const SDL_Rect * rect, int mode); //mode: 0 - sepia, 1 - grayscale void startTextInput(SDL_Rect * where); void stopTextInput(); void setColorKey(SDL_Surface * surface, SDL_Color color); ///set key-color to 0,255,255 void setDefaultColorKey(SDL_Surface * surface); ///set key-color to 0,255,255 only if it exactly mapped void setDefaultColorKeyPresize(SDL_Surface * surface); } vcmi-0.99+git20190113.f06c8a87/client/gui/SDL_Pixels.h000066400000000000000000000166141342332007200213430ustar00rootroot00000000000000/* * SDL_Pixels.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #pragma once #include #include "SDL_Extensions.h" // for accessing channels from pixel in format-independent way //should be as fast as accessing via pointer at least for 3-4 bytes per pixel namespace Channels { // channel not present in this format struct channel_empty { static STRONG_INLINE void set(Uint8*, Uint8) {} static STRONG_INLINE Uint8 get(const Uint8 *) { return 255;} }; // channel which uses whole pixel template struct channel_pixel { static STRONG_INLINE void set(Uint8 *ptr, Uint8 value) { ptr[offset] = value; } static STRONG_INLINE Uint8 get(const Uint8 *ptr) { return ptr[offset]; } }; // channel which uses part of pixel template struct channel_subpx { static void STRONG_INLINE set(Uint8 *ptr, Uint8 value) { Uint16 * const pixel = (Uint16*)ptr; Uint8 subpx = value >> (8 - bits); *pixel = (*pixel & ~mask) | ((subpx << shift) & mask ); } static Uint8 STRONG_INLINE get(const Uint8 *ptr) { Uint8 subpx = (*((Uint16 *)ptr) & mask) >> shift; return (subpx << (8 - bits)) | (subpx >> (2*bits - 8)); } }; template struct px { static channel_empty r; static channel_empty g; static channel_empty b; static channel_empty a; }; #if (SDL_BYTEORDER == SDL_BIG_ENDIAN) template<> struct px<4> { static channel_pixel<1> r; static channel_pixel<2> g; static channel_pixel<3> b; static channel_pixel<0> a; }; template<> struct px<3> { static channel_pixel<0> r; static channel_pixel<1> g; static channel_pixel<2> b; static channel_empty a; }; #else template<> struct px<4> { static channel_pixel<2> r; static channel_pixel<1> g; static channel_pixel<0> b; static channel_pixel<3> a; }; template<> struct px<3> { static channel_pixel<2> r; static channel_pixel<1> g; static channel_pixel<0> b; static channel_empty a; }; #endif template<> struct px<2> { static channel_subpx<5, 0xF800, 11> r; static channel_subpx<6, 0x07E0, 5 > g; static channel_subpx<5, 0x001F, 0 > b; static channel_empty a; }; } template struct ColorPutter<2, incrementPtr> { static STRONG_INLINE void PutColor(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B); static STRONG_INLINE void PutColor(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B, const Uint8 & A); static STRONG_INLINE void PutColorAlphaSwitch(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B, const Uint8 & A); static STRONG_INLINE void PutColor(Uint8 *&ptr, const SDL_Color & Color); static STRONG_INLINE void PutColorAlpha(Uint8 *&ptr, const SDL_Color & Color); static STRONG_INLINE void PutColorRow(Uint8 *&ptr, const SDL_Color & Color, size_t count); }; template STRONG_INLINE void ColorPutter::PutColorAlpha(Uint8 *&ptr, const SDL_Color & Color) { PutColor(ptr, Color.r, Color.g, Color.b, Color.a); } template STRONG_INLINE void ColorPutter::PutColor(Uint8 *&ptr, const SDL_Color & Color) { PutColor(ptr, Color.r, Color.g, Color.b); } template STRONG_INLINE void ColorPutter::PutColorAlphaSwitch(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B, const Uint8 & A) { switch (A) { case 0: ptr += bpp * incrementPtr; return; case 255: PutColor(ptr, R, G, B); return; case 128: // optimized PutColor(ptr, ((Uint16)R + Channels::px::r.get(ptr)) >> 1, ((Uint16)G + Channels::px::g.get(ptr)) >> 1, ((Uint16)B + Channels::px::b.get(ptr)) >> 1); return; default: PutColor(ptr, R, G, B, A); return; } } template STRONG_INLINE void ColorPutter::PutColor(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B, const Uint8 & A) { PutColor(ptr, ((((Uint32)R - (Uint32)Channels::px::r.get(ptr))*(Uint32)A) >> 8 ) + (Uint32)Channels::px::r.get(ptr), ((((Uint32)G - (Uint32)Channels::px::g.get(ptr))*(Uint32)A) >> 8 ) + (Uint32)Channels::px::g.get(ptr), ((((Uint32)B - (Uint32)Channels::px::b.get(ptr))*(Uint32)A) >> 8 ) + (Uint32)Channels::px::b.get(ptr)); } template STRONG_INLINE void ColorPutter::PutColor(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B) { static_assert(incrementPtr >= -1 && incrementPtr <= +1, "Invalid incrementPtr value!"); if (incrementPtr < 0) ptr -= bpp; Channels::px::r.set(ptr, R); Channels::px::g.set(ptr, G); Channels::px::b.set(ptr, B); Channels::px::a.set(ptr, 255); if (incrementPtr > 0) ptr += bpp; } template STRONG_INLINE void ColorPutter::PutColorRow(Uint8 *&ptr, const SDL_Color & Color, size_t count) { if (count) { Uint8 *pixel = ptr; PutColor(ptr, Color.r, Color.g, Color.b); for (size_t i=0; i STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColor(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B) { if(incrementPtr == -1) ptr -= 2; Uint16 * const px = (Uint16*)ptr; *px = (B>>3) + ((G>>2) << 5) + ((R>>3) << 11); //drop least significant bits of 24 bpp encoded color if(incrementPtr == 1) ptr += 2; //bpp } template STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColorAlphaSwitch(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B, const Uint8 & A) { switch (A) { case 0: ptr += 2 * incrementPtr; return; case 255: PutColor(ptr, R, G, B); return; default: PutColor(ptr, R, G, B, A); return; } } template STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColor(Uint8 *&ptr, const Uint8 & R, const Uint8 & G, const Uint8 & B, const Uint8 & A) { const int rbit = 5, gbit = 6, bbit = 5; //bits per color const int rmask = 0xF800, gmask = 0x7E0, bmask = 0x1F; const int rshift = 11, gshift = 5, bshift = 0; const Uint8 r5 = (*((Uint16 *)ptr) & rmask) >> rshift, b5 = (*((Uint16 *)ptr) & bmask) >> bshift, g5 = (*((Uint16 *)ptr) & gmask) >> gshift; const Uint32 r8 = (r5 << (8 - rbit)) | (r5 >> (2*rbit - 8)), g8 = (g5 << (8 - gbit)) | (g5 >> (2*gbit - 8)), b8 = (b5 << (8 - bbit)) | (b5 >> (2*bbit - 8)); PutColor(ptr, (((R-r8)*A) >> 8) + r8, (((G-g8)*A) >> 8) + g8, (((B-b8)*A) >> 8) + b8); } template STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColorAlpha(Uint8 *&ptr, const SDL_Color & Color) { PutColor(ptr, Color.r, Color.g, Color.b, Color.a); } template STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColor(Uint8 *&ptr, const SDL_Color & Color) { PutColor(ptr, Color.r, Color.g, Color.b); } template STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColorRow(Uint8 *&ptr, const SDL_Color & Color, size_t count) { //drop least significant bits of 24 bpp encoded color Uint16 pixel = (Color.b>>3) + ((Color.g>>2) << 5) + ((Color.r>>3) << 11); for (size_t i=0; i H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATx\u{ V+ !B@/lCRP@6W QI)aK_v &ƱIH ; `#+$E}Ѱ̽};3v;;s|tw{~~1 `0 Xܣ`у0Z>RXRWp>74Ā9-|2|`sT{s0Bs׾\OB-J)[rKGM->si GÚB7iԿvve`~yC9gX[k ^)!$N{{v ҉}xMf?*ʁ4(ЪV>Iu>e )Z Q"ߒ&6N4 L惨 !3 g[8:`Y %?&b -_洒<7w+׍邵FY3 {:́ @~E q;s_k.=$]ǡ$FD +v~k (ĺvHkG,;iqQ->g]&V啝9ob$5\9U84Q"-q뒟1ZuЯ^WSW!{1FFNK `,٠>!ѯ C :+Z's%Mt h`\B_)_%ušP~vhr^iZen-2ٌ¿ +k>I m֮j iIjPN`Y5 C.k-LYQ`ު@hJK]e! 9$4u. s gHaZ V]W _ 1$FX?K Y7 ͔!3 E@06K.Ju?EJ;V<+ȗC?g(_R_g4DuWT:к p4@vo0H-]FW1`oLѵ*%yFk ]N慒s00wXUTKn (Z%-`YF5BɁ% 9QK[9['aԙ)-1BB`I!w-[_&IR)c1{ yd!d &rԕ{iq'$cr^ MY[r`K%ye]Qc`` +c-nƗ:H. U\H%-%Rj>,vT5_۶dcZ؞@Un(O,׵¿_Y -M+U(A7rOa- ̹ G^>c@E>Kf"hZ c}lu*/ U\^jQ?T PAmG@ 3r0N;9e%m'K6 zt <09Ěd u.7)Ⲧ=˱Ra2׭ʝ Zi6 y ]/Gh֤ ^Z>c ^O5k0EKqkc*X7a^@׉yk~1BV̀BݹQn V\\?t_(Zԇa?GQ_@[AaS hb`h`i9Ylw٤|Ome_#k+M${G,Jm?mي`>lN`ш!% "]S%iҺ17m Vhrv XO&LM'{`i 8EUkcw֣%k}庼 䶚Цٱ{Z X@- $$C ۪VPvdSIHah;]60_lӯ%_#;|IZ|r~=c,RpTyK̀VfZΥYoYqNg_ c`.vLX1#)zAi{XL@Ns`z~cgG{"_+%kZu[(_ 'X_Jr9% @"c7Dc,I@#4BGZ/9Osvi];$xsGc3@:=eY2iOKydf)AbXZ5|n1 qzv!+STcV&g35隡|waa(ݳ -r⛆}.\2__̓eNUq9L Eg8'O# I7_cxq`c`NJ?4%8{S0V몑+E@]]w Y KKfA*E'LO-bormڽBc+E7˫|{W{u0f}L3Gݲ];s {Lͦ֍%%0_chG`H }//;_e4z0 n 0X >V↧2$f@nR&ltA* r&Y}¾5},=u@iCbȅE|Crt.tNh|Z5_C*# t:`L! =O*b_zߨPevbLk »E01BB_ch6B=n 1 bZN`ك $H'A,~Ao9}}ط·ZAmRۗ &:f+s>,>?.QO}k~Lp)h'5R.#Q`NKA vJD0)LŽ&~-䇪9Iʱ4+FՆ!_^ʙ)++T,G8N/SFw5к 2$nWݿ#W>H,[޳ aS`y`¨34&@c Hk@{2A. Hq&᭻]"!Y`)LԄ|3r]}ŵk}Sb?f|ZeM_ۭ(ݹ>W$+mlqp]hLrnV `D 8Y1T P$R%I\pdOg)ڥ#nhRnR12 4 ˝b)c߻1^ g)E|ɺsUcYM|1_"s㗊z>+7,Xk,|6; =`"+ C~Qg&t| u]s5cjO+Gw՘1 ƬK TnHqe!S@z[RKA)2J4H!¬)7[j.[Y͋S\Ws ~rEneU&X,0-.p[i,|C[1Ep,l烣 ˥~l1,,j)vOIbʽBf@j a`5b B&ib v9R4,9DTKkcFk+|h:Y8_bܙ>seh!kםdG-LU*b`LU- WrL9>~JF}t]q.'n& ߋ.XsjX ءI|\Nc**Tpy$IM"^\-υ泥1%-Bzx*M Տ=H~cJ)׻3]uy0qrP(Shr*(XeSw h @H& i0ƀ1]h2Rdjw^y ,h @vEne=ϪFw cwΗ`͞29~UsguEy:uH7jg0Rc-b@vRQc*U-[|?n/2 0H12&AS@}Z 0h'S-ԧ!X+\-KI~]c?k7MG!;`"k,1GY}SB?F>0Oˊ/sӟ6.Nv"4Z:Sa h[Qnlaz?$ZZ/P s\e#@cX́u Vb`r$Ay 52"~Ws\8[kļTLj|i#+.n ^νD|`܋Ywf3r~q-BQʛ}$FD\^6Wv5s}I1=䮗n73.wi 0 4急@c I0<.T %uFvZ3@{$HeXXle)vHދ<1oYWUܗJا2|)Bںb~U_ά/rbv_(7;^XTB.ZqL/u)Z(GO߸q㲏8vԸx֕\+̀~}Tԛh+]}eцݡ(,7mt"*$MJ}u4:J+}ߤ*Z/5\Nٚo5]b?VK. 8O]^_U8DcDpWo5k]vAU;X%],4f T[]%Mpʱэ˖ W>|xĉ;zꩽ'Op?x嶟]/7{WGηo}/o/'ýwq-e=ΜJO𛽳@*B΁)/ u X4 4@cP>!q: E~)汩3UK}.8>)bFjڇ\qķ=_|S?~푧Ż'5xoѣW^zb|/C޿SG\R333z{Zg;JH>D:3r+KHʳlVoY|oxn|S}<#9zt߁nڹk׮7mZq㦵N[~ 3_nժU+wbˑ#G=/?G_|;.A .Fs v$0Bwn\kޯ`RM)Z Ri.M ?O'k+}LJ^r[W^O>9-5-g sh"\촿QoT?wݟo??:z}|u]qnnn(5k[+]Ǧ|ӟsO