vcmi-0.98/000077500000000000000000000000001250671757600124305ustar00rootroot00000000000000vcmi-0.98/.gitignore000066400000000000000000000006061250671757600144220ustar00rootroot00000000000000/client/vcmiclient /server/vcmiserver /launcher/vcmilauncher /launcher/vcmilauncher_automoc.cpp *.dll *.exe *.depend *.o *.a *.so *.res *.layout *.pro.user *.pro.user.* /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.* vcmi-0.98/.travis.yml000066400000000000000000000036411250671757600145450ustar00rootroot00000000000000language: cpp before_install: #new boost - sudo add-apt-repository --yes ppa:boost-latest/ppa #new GCC - sudo add-apt-repository --yes ppa:ubuntu-toolchain-r/test #new Clang - sudo add-apt-repository --yes ppa:h-rayflood/llvm #new SDL2 - sudo add-apt-repository --yes ppa:zoogie/sdl2-snapshots #new Qt - sudo add-apt-repository --yes ppa:beineri/opt-qt532 #new FFmpeg - sudo add-apt-repository --yes ppa:jon-severinsson/ffmpeg #new CMake - sudo add-apt-repository --yes ppa:andykimpe/cmake - sudo apt-get update -qq - sudo apt-get install -qq $SUPPORT - sudo apt-get install -qq $PACKAGE - sudo apt-get install -qq cmake libboost1.55-all-dev zlib1g-dev - sudo apt-get install -qq libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev - sudo apt-get install -qq libavformat-dev libswscale-dev - sudo apt-get install -qq qt53declarative #setup compiler - source /opt/qt53/bin/qt53-env.sh - export CC=${REAL_CC} CXX=${REAL_CXX} before_script: - mkdir build - cd build - cmake .. script: - make env: - ignore=this matrix: exclude: - env: ignore=this include: - compiler: clang # fails all the time - missing packages? env: REAL_CC=clang-3.2 REAL_CXX=clang++-3.2 PACKAGE=clang-3.2 SUPPORT=g++-4.8 - compiler: clang env: REAL_CC=clang-3.3 REAL_CXX=clang++-3.3 PACKAGE=clang-3.3 SUPPORT=g++-4.8 - compiler: clang env: REAL_CC=clang-3.4 REAL_CXX=clang++-3.4 PACKAGE=clang-3.4 SUPPORT=g++-4.8 #- compiler: gcc # fails due to running out of memory - vcmi need too much of it for successfull compilation # env: REAL_CC=gcc-4.7 REAL_CXX=g++-4.7 PACKAGE=g++-4.7 SUPPORT= #- compiler: gcc # same as 4.7 # env: REAL_CC=gcc-4.8 REAL_CXX=g++-4.8 PACKAGE=g++-4.8 SUPPORT= notifications: email: recipients: - vcmi.fail@mixaill.tk - saven.ivan@gmail.com on_success: change on_failure: always vcmi-0.98/AI/000077500000000000000000000000001250671757600127215ustar00rootroot00000000000000vcmi-0.98/AI/BattleAI/000077500000000000000000000000001250671757600143465ustar00rootroot00000000000000vcmi-0.98/AI/BattleAI/BattleAI.cbp000066400000000000000000000056361250671757600164730ustar00rootroot00000000000000 vcmi-0.98/AI/BattleAI/BattleAI.cpp000066400000000000000000000515531250671757600165100ustar00rootroot00000000000000#include "StdInc.h" #include "../../lib/AI_Base.h" #include "BattleAI.h" #include "../../lib/BattleState.h" #include "../../CCallback.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/VCMI_Lib.h" using boost::optional; static shared_ptr cbc; #define LOGL(text) print(text) #define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) struct Priorities { double manaValue; double generalResourceValueModifier; std::vector resourceTypeBaseValues; std::function stackEvaluator; Priorities() { manaValue = 0.; generalResourceValueModifier = 1.; range::copy(VLC->objh->resVals, std::back_inserter(resourceTypeBaseValues)); stackEvaluator = [](const CStack*){ return 1.0; }; } }; Priorities *priorities = nullptr; namespace { int distToNearestNeighbour(BattleHex hex, const ReachabilityInfo::TDistances& dists, BattleHex *chosenHex = nullptr) { int ret = 1000000; for(BattleHex 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->position, dists) < distToNearestNeighbour(ei2.s->position, dists); } } 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; } CBattleAI::CBattleAI(void) : side(-1) { print("created"); } CBattleAI::~CBattleAI(void) { print("destroyed"); if(cb) { //Restore previous state of CB - it may be shared with the main AI (like VCAI) cb->waitTillRealize = wasWaitingForRealize; cb->unlockGsWhenWaiting = wasUnlockingGs; } } void CBattleAI::init(shared_ptr CB) { print("init called, saving ptr to IBattleCallback"); cbc = cb = CB; playerID = *CB->getPlayerID();; //TODO should be sth in callback wasWaitingForRealize = cb->waitTillRealize; wasUnlockingGs = CB->unlockGsWhenWaiting; CB->waitTillRealize = true; CB->unlockGsWhenWaiting = false; } static bool thereRemainsEnemy() { return !cbc->battleIsFinished(); } BattleAction CBattleAI::activeStack( const CStack * stack ) { LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()) ; cbc = cb; //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) try { print("activeStack called for " + stack->nodeName()); 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->firstHPleft) 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 } if(cb->battleCanCastSpell()) attemptCastingSpell(); if(!thereRemainsEnemy()) return BattleAction(); if(auto action = considerFleeingOrSurrendering()) return *action; if(cb->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY).empty()) { //We apparently won battle by casting spell, return defend... (accessing cb may cause trouble) return BattleAction::makeDefend(stack); } PotentialTargets targets(stack); if(targets.possibleAttacks.size()) { auto hlp = targets.bestAction(); if(hlp.attack.shooting) return BattleAction::makeShotAttack(stack, hlp.enemy); else return BattleAction::makeMeleeAttack(stack, hlp.enemy, hlp.tile); } else { if(stack->waited()) { ThreatMap threatsToUs(stack); auto dists = cbc->battleGetDistances(stack); const EnemyInfo &ei= *range::min_element(targets.unreachableEnemies, std::bind(isCloser, _1, _2, std::ref(dists))); if(distToNearestNeighbour(ei.s->position, dists) < GameConstants::BFIELD_SIZE) { return goTowards(stack, ei.s->position); } } else { return BattleAction::makeWait(stack); } } } catch(std::exception &e) { logAi->errorStream() << "Exception occurred in " << __FUNCTION__ << " " << e.what(); } return BattleAction::makeDefend(stack); } void CBattleAI::actionFinished(const BattleAction &action) { print("actionFinished called"); } void CBattleAI::actionStarted(const BattleAction &action) { print("actionStarted called"); } void CBattleAI::battleAttack(const BattleAttack *ba) { print("battleAttack called"); } void CBattleAI::battleStacksAttacked(const std::vector & bsa) { print("battleStacksAttacked called"); } void CBattleAI::battleEnd(const BattleResult *br) { print("battleEnd called"); } void CBattleAI::battleNewRoundFirst(int round) { print("battleNewRoundFirst called"); } void CBattleAI::battleNewRound(int round) { print("battleNewRound called"); } void CBattleAI::battleStackMoved(const CStack * stack, std::vector dest, int distance) { print("battleStackMoved called");; } void CBattleAI::battleSpellCast(const BattleSpellCast *sc) { print("battleSpellCast called"); } void CBattleAI::battleStacksEffectsSet(const SetStackEffect & sse) { print("battleStacksEffectsSet called"); } void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side) { print("battleStart called"); side = Side; } void CBattleAI::battleStacksHealedRes(const std::vector > & healedStacks, bool lifeDrain, bool tentHeal, si32 lifeDrainFrom) { print("battleStacksHealedRes called"); } void CBattleAI::battleNewStackAppeared(const CStack * stack) { print("battleNewStackAppeared called"); } void CBattleAI::battleObstaclesRemoved(const std::set & removedObstacles) { print("battleObstaclesRemoved called"); } void CBattleAI::battleCatapultAttacked(const CatapultAttack & ca) { print("battleCatapultAttacked called"); } void CBattleAI::battleStacksRemoved(const BattleStacksRemoved & bsr) { print("battleStacksRemoved called"); } void CBattleAI::print(const std::string &text) const { logAi->traceStream() << "CBattleAI [" << this <<"]: " << text; } BattleAction CBattleAI::goTowards(const CStack * stack, BattleHex destination) { assert(destination.isValid()); auto avHexes = cb->battleGetAvailableHexes(stack, false); auto reachability = cb->getReachability(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->warnStream() << "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]; } } } BattleAction CBattleAI::useCatapult(const CStack * stack) { throw std::runtime_error("The method or operation is not implemented."); } enum SpellTypes { OFFENSIVE_SPELL, TIMED_EFFECT, OTHER }; SpellTypes spellType(const CSpell *spell) { if (spell->isOffensiveSpell()) return OFFENSIVE_SPELL; if (spell->hasEffects()) return TIMED_EFFECT; return OTHER; } struct PossibleSpellcast { const CSpell *spell; BattleHex dest; }; struct CurrentOffensivePotential { std::map ourAttacks; std::map enemyAttacks; CurrentOffensivePotential(ui8 side) { for(auto stack : cbc->battleGetStacks()) { if(stack->attackerOwned == !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; } }; // // //set has its own order, so remove_if won't work. TODO - reuse for map // 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); // } // } void CBattleAI::attemptCastingSpell() { LOGL("Casting spells sounds like fun. Let's see..."); auto hero = cb->battleGetMyHero(); //auto known = cb->battleGetFightingHero(side); //Get all spells we can cast std::vector possibleSpells; vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [this] (const CSpell *s) -> bool { auto problem = cbc->battleCanCastThisSpell(s); return problem == ESpellCastProblem::OK; }); LOGFL("I can cast %d spells.", possibleSpells.size()); vstd::erase_if(possibleSpells, [](const CSpell *s) {return spellType(s) == OTHER; }); LOGFL("I know about workings of %d of them.", possibleSpells.size()); //Get possible spell-target pairs std::vector possibleCasts; for(auto spell : possibleSpells) { for(auto hex : getTargetsToConsider(spell)) { PossibleSpellcast ps = {spell, hex}; possibleCasts.push_back(ps); } } LOGFL("Found %d spell-target combinations.", possibleCasts.size()); if(possibleCasts.empty()) return; std::map valueOfStack; for(auto stack : cb->battleGetStacks()) { PotentialTargets pt(stack); valueOfStack[stack] = pt.bestActionValue(); } auto evaluateSpellcast = [&] (const PossibleSpellcast &ps) -> int { const int skillLevel = hero->getSpellSchoolLevel(ps.spell); const int spellPower = hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER); switch(spellType(ps.spell)) { case OFFENSIVE_SPELL: { int damageDealt = 0, damageReceived = 0; auto stacksSuffering = ps.spell->getAffectedStacks(cb.get(), ECastingMode::HERO_CASTING, playerID, skillLevel, ps.dest, hero); if(stacksSuffering.empty()) return -1; for(auto stack : stacksSuffering) { const int dmg = ps.spell->calculateDamage(hero, stack, skillLevel, spellPower); if(stack->owner == playerID) damageReceived += dmg; else damageDealt += dmg; } const int damageDiff = damageDealt - damageReceived; LOGFL("Casting %s on hex %d would deal %d damage points among %d stacks.", ps.spell->name % ps.dest % damageDiff % stacksSuffering.size()); //TODO tactic effect too return damageDiff; } case TIMED_EFFECT: { StackWithBonuses swb; swb.stack = cb->battleGetStackByPos(ps.dest); if(!swb.stack) return -1; Bonus pseudoBonus; pseudoBonus.sid = ps.spell->id; pseudoBonus.val = skillLevel; pseudoBonus.turnsRemain = 1; //TODO CStack::stackEffectToFeature(swb.bonusesToAdd, pseudoBonus); HypotheticChangesToBattleState state; state.bonusesOfStacks[swb.stack] = &swb; PotentialTargets pt(swb.stack, state); auto newValue = pt.bestActionValue(); auto oldValue = valueOfStack[swb.stack]; auto gain = newValue - oldValue; if(swb.stack->owner != playerID) //enemy gain = -gain; LOGFL("Casting %s on %s would improve the stack by %d points (from %d to %d)", ps.spell->name % swb.stack->nodeName() % gain % (oldValue) % (newValue)); return gain; } default: assert(0); return 0; } }; auto castToPerform = *vstd::maxElementByFun(possibleCasts, evaluateSpellcast); LOGFL("Best spell is %s. Will cast.", castToPerform.spell->name); BattleAction spellcast; spellcast.actionType = Battle::HERO_SPELL; spellcast.additionalInfo = castToPerform.spell->id; spellcast.destinationTile = castToPerform.dest; spellcast.side = side; spellcast.stackNumber = (!side) ? -1 : -2; cb->battleMakeAction(&spellcast); } std::vector CBattleAI::getTargetsToConsider( const CSpell *spell ) const { if(spell->getTargetType() == CSpell::NO_TARGET) { //Spell can be casted anywhere, all hexes are potentially considerable. std::vector ret; for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) if(BattleHex(i).isAvailable()) ret.push_back(i); return ret; } else { //TODO when massive effect -> doesn't matter where cast return cbc->battleGetPossibleTargets(playerID, spell); } } boost::optional CBattleAI::considerFleeingOrSurrendering() { if(cb->battleCanSurrender(playerID)) { } if(cb->battleCanFlee()) { } return boost::none; } ThreatMap::ThreatMap(const CStack *Endangered) : endangered(Endangered) { sufferedDamage.fill(0); for(const CStack *enemy : cbc->battleGetStacks()) { //Consider only stacks of different owner if(enemy->attackerOwned == endangered->attackerOwned) continue; //Look-up which tiles can be melee-attacked std::array meleeAttackable; meleeAttackable.fill(false); auto enemyReachability = cbc->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(cbc->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 = cbc->calculateDmgRange(bai); return (dmg.first + dmg.second)/2; }); } } const TBonusListPtr StackWithBonuses::getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root /*= nullptr*/, const std::string &cachingStr /*= ""*/) const { TBonusListPtr ret = make_shared(); const TBonusListPtr originalList = stack->getAllBonuses(selector, limit, root, cachingStr); range::copy(*originalList, std::back_inserter(*ret)); for(auto &bonus : bonusesToAdd) { if(selector(&bonus) && (!limit || !limit(&bonus))) ret->push_back(&bonus); } //TODO limiters? return ret; } int AttackPossibility::damageDiff() const { if (!priorities) priorities = new Priorities; const auto dealtDmgValue = priorities->stackEvaluator(enemy) * damageDealt; const auto receivedDmgValue = priorities->stackEvaluator(attack.attacker) * damageReceived; return dealtDmgValue - receivedDmgValue; } int AttackPossibility::attackValue() const { return damageDiff() + tacticImpact; } AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo &AttackInfo, const HypotheticChangesToBattleState &state, BattleHex hex) { auto attacker = AttackInfo.attacker; auto enemy = AttackInfo.defender; const int remainingCounterAttacks = getValOr(state.counterAttacksLeft, enemy, enemy->counterAttacks); const bool counterAttacksBlocked = attacker->hasBonusOfType(Bonus::BLOCKS_RETALIATION) || enemy->hasBonusOfType(Bonus::NO_RETALIATION); const int totalAttacks = 1 + AttackInfo.attackerBonuses->getBonuses(Selector::type(Bonus::ADDITIONAL_ATTACK), (Selector::effectRange (Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_MELEE_FIGHT))))->totalValue(); AttackPossibility ap = {enemy, hex, AttackInfo, 0, 0, 0}; auto curBai = AttackInfo; //we'll modify here the stack counts for(int i = 0; i < totalAttacks; i++) { std::pair retaliation(0,0); auto attackDmg = cbc->battleEstimateDamage(curBai, &retaliation); ap.damageDealt = (attackDmg.first + attackDmg.second) / 2; ap.damageReceived = (retaliation.first + retaliation.second) / 2; if(remainingCounterAttacks <= i || counterAttacksBlocked) ap.damageReceived = 0; curBai.attackerCount = attacker->count - attacker->countKilledByAttack(ap.damageReceived).first; curBai.defenderCount = enemy->count - enemy->countKilledByAttack(ap.damageDealt).first; if(!curBai.attackerCount) break; //TODO what about defender? should we break? but in pessimistic scenario defender might be alive } //TODO other damage related to attack (eg. fire shield and other abilities) //Limit damages by total stack health vstd::amin(ap.damageDealt, enemy->count * enemy->MaxHealth() - (enemy->MaxHealth() - enemy->firstHPleft)); vstd::amin(ap.damageReceived, attacker->count * attacker->MaxHealth() - (attacker->MaxHealth() - attacker->firstHPleft)); return ap; } PotentialTargets::PotentialTargets(const CStack *attacker, const HypotheticChangesToBattleState &state /*= HypotheticChangesToBattleState()*/) { auto dists = cbc->battleGetDistances(attacker); auto avHexes = cbc->battleGetAvailableHexes(attacker, false); for(const CStack *enemy : cbc->battleGetStacks()) { //Consider only stacks of different owner if(enemy->attackerOwned == attacker->attackerOwned) continue; auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility { auto bai = BattleAttackInfo(attacker, enemy, shooting); bai.attackerBonuses = getValOr(state.bonusesOfStacks, bai.attacker, bai.attacker); bai.defenderBonuses = getValOr(state.bonusesOfStacks, bai.defender, bai.defender); if(hex.isValid()) { assert(dists[hex] <= attacker->Speed()); bai.chargedFields = dists[hex]; } return AttackPossibility::evaluate(bai, state, hex); }; if(cbc->battleCanShoot(attacker, enemy->position)) { possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID)); } else { for(BattleHex hex : avHexes) if(CStack::isMeleeAttackPossible(attacker, enemy, hex)) possibleAttacks.push_back(GenerateAttackInfo(false, hex)); if(!vstd::contains_if(possibleAttacks, [=](const AttackPossibility &pa) { return pa.enemy == enemy; })) unreachableEnemies.push_back(enemy); } } } 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(); } ); } int PotentialTargets::bestActionValue() const { if(possibleAttacks.empty()) return 0; return bestAction().attackValue(); } void EnemyInfo::calcDmg(const CStack * ourStack) { TDmgRange retal, dmg = cbc->battleEstimateDamage(ourStack, s, &retal); adi = (dmg.first + dmg.second) / 2; adr = (retal.first + retal.second) / 2; } vcmi-0.98/AI/BattleAI/BattleAI.h000066400000000000000000000134601250671757600161500ustar00rootroot00000000000000#pragma once #include "../../lib/BattleHex.h" #include "../../lib/HeroBonus.h" #include "../../lib/CBattleCallback.h" class CSpell; class StackWithBonuses : public IBonusBearer { public: const CStack *stack; mutable std::vector bonusesToAdd; virtual const TBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr, const std::string &cachingStr = "") const override; }; struct EnemyInfo { const CStack * s; int adi, adr; std::vector attackFrom; //for melee fight EnemyInfo(const CStack * _s) : s(_s) {} void calcDmg(const CStack * ourStack); bool operator==(const EnemyInfo& ei) const { return s == ei.s; } }; //FIXME: unused function /* 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++) BOOST_FOREACH(BattleHex neighbour, (i ? h2 : h1).neighbouringTiles()) if(const CStack *s = cbc->battleGetStackByPos(neighbour)) if(s->getCreature()->isShooting()) shooters[i]++; return shooters[0] < shooters[1]; } */ struct ThreatMap { std::array, GameConstants::BFIELD_SIZE> threatMap; // [hexNr] -> enemies able to strike const CStack *endangered; std::array sufferedDamage; ThreatMap(const CStack *Endangered); }; struct HypotheticChangesToBattleState { std::map bonusesOfStacks; std::map counterAttacksLeft; }; struct AttackPossibility { const CStack *enemy; //redundant (to attack.defender) but looks nice BattleHex tile; //tile from which we attack BattleAttackInfo attack; int damageDealt; int damageReceived; //usually by counter-attack int tacticImpact; int damageDiff() const; int attackValue() const; static AttackPossibility evaluate(const BattleAttackInfo &AttackInfo, const HypotheticChangesToBattleState &state, BattleHex hex); }; 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; } struct PotentialTargets { std::vector possibleAttacks; std::vector unreachableEnemies; //std::function GenerateAttackInfo; //args: shooting, destHex PotentialTargets(){}; PotentialTargets(const CStack *attacker, const HypotheticChangesToBattleState &state = HypotheticChangesToBattleState()); AttackPossibility bestAction() const; int bestActionValue() const; }; class CBattleAI : public CBattleGameInterface { int side; shared_ptr cb; //Previous setting of cb bool wasWaitingForRealize, wasUnlockingGs; void print(const std::string &text) const; public: CBattleAI(void); ~CBattleAI(void); void init(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) 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 battleStacksHealedRes(const std::vector > & healedStacks, bool lifeDrain, bool tentHeal, si32 lifeDrainFrom) override; //called when stacks are healed / resurrected first element of pair - stack id, second - healed hp void battleNewStackAppeared(const CStack * stack) override; //not called at the beginning of a battle or by resurrection; called eg. when elemental is summoned void battleObstaclesRemoved(const std::set & removedObstacles) override; //called when a certain set of obstacles is removed from batlefield; IDs of them are given void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleStacksRemoved(const BattleStacksRemoved & bsr) override; //called when certain stack is completely removed from battlefield BattleAction goTowards(const CStack * stack, BattleHex hex ); BattleAction useCatapult(const CStack * stack); boost::optional considerFleeingOrSurrendering(); void attemptCastingSpell(); std::vector getTargetsToConsider(const CSpell *spell) const; }; vcmi-0.98/AI/BattleAI/BattleAI.vcxproj000066400000000000000000000174251250671757600174210ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 {C0300513-E845-43B4-9A4F-E8817EAEF57C} BattleAI DynamicLibrary true MultiByte v120_xp DynamicLibrary true MultiByte v120_xp DynamicLibrary false true MultiByte v120_xp DynamicLibrary false true MultiByte v120_xp $(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 /Zm159 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) ..\..\..\libs;..\.. Use StdInc.h /Zm159 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) %(PreprocessorDefinitions) Create StdInc.h Create Create Create vcmi-0.98/AI/BattleAI/CMakeLists.txt000066400000000000000000000010731250671757600171070ustar00rootroot00000000000000project(battleAI) cmake_minimum_required(VERSION 2.6) include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib) set(battleAI_SRCS StdInc.cpp BattleAI.cpp main.cpp ) add_library(BattleAI SHARED ${battleAI_SRCS}) target_link_libraries(BattleAI vcmi) set_target_properties(BattleAI PROPERTIES ${PCH_PROPERTIES}) cotire(BattleAI) if (NOT APPLE) # Already inside vcmiclient bundle install(TARGETS BattleAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR}) endif() vcmi-0.98/AI/BattleAI/StdInc.cpp000066400000000000000000000000661250671757600162400ustar00rootroot00000000000000// Creates the precompiled header #include "StdInc.h"vcmi-0.98/AI/BattleAI/StdInc.h000066400000000000000000000003541250671757600157050ustar00rootroot00000000000000#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.98/AI/BattleAI/main.cpp000066400000000000000000000012641250671757600160010ustar00rootroot00000000000000#include "StdInc.h" #include "../../lib/AI_Base.h" #include "BattleAI.h" #ifdef __GNUC__ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif #ifdef VCMI_ANDROID #define GetGlobalAiVersion BattleAI_GetGlobalAiVersion #define GetAiName BattleAI_GetAiName #define GetNewBattleAI BattleAI_GetNewBattleAI #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(shared_ptr &out) { out = make_shared(); } vcmi-0.98/AI/CMakeLists.txt000066400000000000000000000006431250671757600154640ustar00rootroot00000000000000project(AI) cmake_minimum_required(VERSION 2.8) find_package(Fuzzylite) if(NOT MSVC) add_definitions(-DFL_CPP11) set(FL_CPP11 ON CACHE BOOL "") endif() if (NOT FL_FOUND) set(FL_BUILD_BINARY OFF CACHE BOOL "") set(FL_BUILD_SHARED OFF CACHE BOOL "") add_subdirectory(FuzzyLite/fuzzylite) endif() add_subdirectory(BattleAI) add_subdirectory(StupidAI) add_subdirectory(EmptyAI) add_subdirectory(VCAI)vcmi-0.98/AI/EmptyAI/000077500000000000000000000000001250671757600142315ustar00rootroot00000000000000vcmi-0.98/AI/EmptyAI/CEmptyAI.cpp000066400000000000000000000024221250671757600163500ustar00rootroot00000000000000#include "StdInc.h" #include "CEmptyAI.h" #include "../../lib/CRandomGenerator.h" void CEmptyAI::init(shared_ptr CB) { cb = CB; human=false; playerID = *cb->getMyColor(); //logAi->infoStream() << "EmptyAI initialized."; } 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, std::vector 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); } vcmi-0.98/AI/EmptyAI/CEmptyAI.h000066400000000000000000000017301250671757600160160ustar00rootroot00000000000000#pragma once #include "../../lib/AI_Base.h" #include "../../CCallback.h" struct HeroMoveDetails; class CEmptyAI : public CGlobalAI { shared_ptr cb; public: void init(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, std::vector exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; }; #define NAME "EmptyAI 0.1" vcmi-0.98/AI/EmptyAI/CMakeLists.txt000066400000000000000000000007351250671757600167760ustar00rootroot00000000000000project(emptyAI) cmake_minimum_required(VERSION 2.6) include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib) set(emptyAI_SRCS CEmptyAI.cpp exp_funcs.cpp ) add_library(EmptyAI SHARED ${emptyAI_SRCS}) target_link_libraries(EmptyAI vcmi) if (NOT APPLE) # Already inside vcmiclient bundle install(TARGETS EmptyAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR}) endif() vcmi-0.98/AI/EmptyAI/EmptyAI.cbp000066400000000000000000000057751250671757600162450ustar00rootroot00000000000000 vcmi-0.98/AI/EmptyAI/EmptyAI.vcxproj000066400000000000000000000226061250671757600171640ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 Create Create Create Create {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837} EmptyAI DynamicLibrary true MultiByte v120_xp DynamicLibrary true MultiByte v120_xp DynamicLibrary false true MultiByte v120_xp DynamicLibrary false true MultiByte v120_xp $(VCMI_Out)\AI\ $(IncludePath) $(LibraryPath) $(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) /Zm130 %(AdditionalOptions) true true true VCMI_lib.lib;%(AdditionalDependencies) $(OutDir)EmptyAI.dll ..\..\..\libs;..\.. Level3 MaxSpeed true true %(AdditionalIncludeDirectories) Use StdInc.h _WINDLL;%(PreprocessorDefinitions) /Zm130 %(AdditionalOptions) true true true VCMI_lib.lib;%(AdditionalDependencies) $(OutDir)EmptyAI.dll vcmi-0.98/AI/EmptyAI/StdInc.cpp000066400000000000000000000000661250671757600161230ustar00rootroot00000000000000// Creates the precompiled header #include "StdInc.h"vcmi-0.98/AI/EmptyAI/StdInc.h000066400000000000000000000003541250671757600155700ustar00rootroot00000000000000#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.98/AI/EmptyAI/exp_funcs.cpp000066400000000000000000000005231250671757600167270ustar00rootroot00000000000000#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(shared_ptr &out) { out = make_shared(); }vcmi-0.98/AI/GeniusAI.brain000066400000000000000000000123421250671757600154040ustar00rootroot00000000000000o 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.98/AI/StupidAI/000077500000000000000000000000001250671757600144035ustar00rootroot00000000000000vcmi-0.98/AI/StupidAI/CMakeLists.txt000066400000000000000000000010731250671757600171440ustar00rootroot00000000000000project(stupidAI) cmake_minimum_required(VERSION 2.6) include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib) set(stupidAI_SRCS StdInc.cpp StupidAI.cpp main.cpp ) add_library(StupidAI SHARED ${stupidAI_SRCS}) target_link_libraries(StupidAI vcmi) set_target_properties(StupidAI PROPERTIES ${PCH_PROPERTIES}) cotire(StupidAI) if (NOT APPLE) # Already inside vcmiclient bundle install(TARGETS StupidAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR}) endif() vcmi-0.98/AI/StupidAI/StdInc.cpp000066400000000000000000000000661250671757600162750ustar00rootroot00000000000000// Creates the precompiled header #include "StdInc.h"vcmi-0.98/AI/StupidAI/StdInc.h000066400000000000000000000003541250671757600157420ustar00rootroot00000000000000#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.98/AI/StupidAI/StupidAI.cbp000066400000000000000000000060541250671757600165600ustar00rootroot00000000000000 vcmi-0.98/AI/StupidAI/StupidAI.cpp000066400000000000000000000221131250671757600165700ustar00rootroot00000000000000#include "StdInc.h" #include "../../lib/AI_Base.h" #include "StupidAI.h" #include "../../lib/BattleState.h" #include "../../CCallback.h" #include "../../lib/CCreatureHandler.h" static shared_ptr cbc; CStupidAI::CStupidAI(void) : side(-1) { print("created"); } CStupidAI::~CStupidAI(void) { print("destroyed"); } void CStupidAI::init(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) {} 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->position, dists) < distToNearestNeighbour(ei2.s->position, 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); std::vector enemiesShootable, enemiesReachable, enemiesUnreachable; if(stack->type->idNumber == CreatureID::CATAPULT) { BattleAction attack; static const std::vector wallHexes = {50, 183, 182, 130, 62, 29, 12, 95}; attack.destinationTile = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault()); attack.actionType = Battle::CATAPULT; attack.additionalInfo = 0; 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->position)) { enemiesShootable.push_back(s); } else { std::vector avHexes = cb->battleGetAvailableHexes(stack, false); for (BattleHex hex : avHexes) { if(CStack::isMeleeAttackPossible(stack, s, hex)) { std::vector::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s); if(i == enemiesReachable.end()) { enemiesReachable.push_back(s); i = enemiesReachable.begin() + (enemiesReachable.size() - 1); } i->attackFrom.push_back(hex); } } if(!vstd::contains(enemiesReachable, s) && s->position.isValid()) enemiesUnreachable.push_back(s); } } 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, *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->position, dists) < GameConstants::BFIELD_SIZE) { return goTowards(stack, ei.s->position); } } return BattleAction::makeDefend(stack); } void CStupidAI::battleAttack(const BattleAttack *ba) { print("battleAttack called"); } void CStupidAI::battleStacksAttacked(const std::vector & bsa) { 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::battleStacksHealedRes(const std::vector > & healedStacks, bool lifeDrain, bool tentHeal, si32 lifeDrainFrom) { print("battleStacksHealedRes called"); } void CStupidAI::battleNewStackAppeared(const CStack * stack) { print("battleNewStackAppeared called"); } void CStupidAI::battleObstaclesRemoved(const std::set & removedObstacles) { print("battleObstaclesRemoved called"); } void CStupidAI::battleCatapultAttacked(const CatapultAttack & ca) { print("battleCatapultAttacked called"); } void CStupidAI::battleStacksRemoved(const BattleStacksRemoved & bsr) { print("battleStacksRemoved called"); } void CStupidAI::print(const std::string &text) const { logAi->traceStream() << "CStupidAI [" << this <<"]: " << text; } BattleAction CStupidAI::goTowards(const CStack * stack, BattleHex destination) { assert(destination.isValid()); auto avHexes = cb->battleGetAvailableHexes(stack, false); auto reachability = cb->getReachability(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->warnStream() << "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(COSer & h, const int version) { //TODO to be implemented with saving/loading during the battles assert(0); } void CStupidAI::loadGame(CISer & h, const int version) { //TODO to be implemented with saving/loading during the battles assert(0); } vcmi-0.98/AI/StupidAI/StupidAI.h000066400000000000000000000055171250671757600162460ustar00rootroot00000000000000#pragma once #include "../../lib/BattleHex.h" class CStupidAI : public CBattleGameInterface { int side; shared_ptr cb; void print(const std::string &text) const; public: CStupidAI(void); ~CStupidAI(void); void init(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) 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 battleStacksHealedRes(const std::vector > & healedStacks, bool lifeDrain, bool tentHeal, si32 lifeDrainFrom) override; //called when stacks are healed / resurrected first element of pair - stack id, second - healed hp void battleNewStackAppeared(const CStack * stack) override; //not called at the beginning of a battle or by resurrection; called eg. when elemental is summoned void battleObstaclesRemoved(const std::set & removedObstacles) override; //called when a certain set of obstacles is removed from batlefield; IDs of them are given void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleStacksRemoved(const BattleStacksRemoved & bsr) override; //called when certain stack is completely removed from battlefield BattleAction goTowards(const CStack * stack, BattleHex hex ); virtual void saveGame(COSer & h, const int version) override; virtual void loadGame(CISer & h, const int version) override; }; vcmi-0.98/AI/StupidAI/StupidAI.vcxproj000066400000000000000000000174071250671757600175130ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B} StupidAI DynamicLibrary true MultiByte v120_xp DynamicLibrary true MultiByte v120_xp DynamicLibrary false true MultiByte v120_xp DynamicLibrary false true MultiByte v120_xp $(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 /Zm150 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) ..\..\..\libs;..\.. Use StdInc.h /Zm150 %(AdditionalOptions) VCMI_lib.lib;%(AdditionalDependencies) %(PreprocessorDefinitions) Create StdInc.h Create Create Create vcmi-0.98/AI/StupidAI/main.cpp000066400000000000000000000012701250671757600160330ustar00rootroot00000000000000#include "StdInc.h" #include "../../lib/AI_Base.h" #include "StupidAI.h" #ifdef __GNUC__ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif #ifdef VCMI_ANDROID #define GetGlobalAiVersion StupidAI_GetGlobalAiVersion #define GetAiName StupidAI_GetAiName #define GetNewBattleAI StupidAI_GetNewBattleAI #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(shared_ptr &out) { out = make_shared(); } vcmi-0.98/AI/VCAI/000077500000000000000000000000001250671757600134435ustar00rootroot00000000000000vcmi-0.98/AI/VCAI/AIUtility.cpp000066400000000000000000000321071250671757600160270ustar00rootroot00000000000000#include "StdInc.h" #include "AIUtility.h" #include "VCAI.h" #include "Fuzzy.h" #include "../../lib/UnlockGuard.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/CBank.h" /* * 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 * */ 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::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 /*= false*/) 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(); } 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 : dirs) { 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 : dirs) { const int3 n = pos + dir; if(cbp->isInTheMap(n)) foo(cbp, pos+dir); } } std::string strFromInt3(int3 pos) { std::ostringstream oss; oss << pos; return oss.str(); } bool CDistanceSorter::operator ()(const CGObjectInstance *lhs, const CGObjectInstance *rhs) { const CGPathNode *ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos()), *rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos()); if(ln->turns != rn->turns) return ln->turns < rn->turns; return (ln->moveRemains > rn->moveRemains); } bool compareMovement(HeroPtr lhs, HeroPtr rhs) { return lhs->movement > rhs->movement; } 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, 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, 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->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->getTacticalAdvantage(visitor, dynamic_cast(cre))); } } } } auto guards = cb->getGuardingCreatures(tile); for (auto cre : guards) { vstd::amax (guardDanger, evaluateDanger(cre) * fh->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: //garrison { 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: { 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) { const ui64 heroStrength = h->getTotalStrength(), dangerStrength = evaluateDanger(tile, *h); if(dangerStrength) { if(heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength) { logAi->traceStream() << boost::format("It's safe for %s to visit tile %s") % h->name % tile; return true; } else return false; } return true; //there's no danger } bool canBeEmbarkmentPoint(const TerrainTile *t, bool fromWater) { //tile must be free of with unoccupied boat return !t->blocked || (!fromWater && t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT); //do not try to board when in water sector } int3 whereToExplore(HeroPtr h) { TimeCheck tc ("where to explore"); int radius = h->getSightRadious(); int3 hpos = h->visitablePos(); SectorMap sm(h); //look for nearby objs -> visit them if they're close enouh const int DIST_LIMIT = 3; 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)) { int3 op = obj->visitablePos(); CGPath p; ai->myCb->getPathsInfo(h.get())->getPath(op, p); if (p.nodes.size() && p.endPos() == op && p.nodes.size() <= DIST_LIMIT) if (ai->isGoodForVisit(obj, h, sm)) nearbyVisitableObjs.push_back(obj); } } } vstd::removeDuplicates (nearbyVisitableObjs); //one object may occupy multiple tiles boost::sort(nearbyVisitableObjs, CDistanceSorter(h.get())); if(nearbyVisitableObjs.size()) return nearbyVisitableObjs.back()->visitablePos(); try //check if nearby tiles allow us to reveal anything - this is quick { return ai->explorationBestNeighbour(hpos, radius, h); } catch(cannotFulfillGoalException &e) { //perform exhaustive search return ai->explorationNewPoint(h); } } bool isBlockedBorderGate(int3 tileToHit) { return cb->getTile(tileToHit)->topVisitableId() == Obj::BORDER_GATE && (dynamic_cast (cb->getTile(tileToHit)->visitableObjects.back()))->wasMyColorVisited (ai->playerID); } int howManyTilesWillBeDiscovered(const int3 &pos, int radious, CCallback * cbp) { //TODO: do not explore dead-end boundaries int ret = 0; for(int x = pos.x - radious; x <= pos.x + radious; x++) { for(int y = pos.y - radious; y <= pos.y + radious; y++) { int3 npos = int3(x,y,pos.z); if(cbp->isInTheMap(npos) && pos.dist2d(npos) - 0.5 < radious && !cbp->isVisible(npos)) { if (!boundaryBetweenTwoPoints (pos, npos, cbp)) ret++; } } } return ret; } bool boundaryBetweenTwoPoints (int3 pos1, int3 pos2, CCallback * cbp) //determines if two points are separated by known barrier { int xMin = std::min (pos1.x, pos2.x); int xMax = std::max (pos1.x, pos2.x); int yMin = std::min (pos1.y, pos2.y); int yMax = std::max (pos1.y, pos2.y); for (int x = xMin; x <= xMax; ++x) { for (int y = yMin; y <= yMax; ++y) { int3 tile = int3(x, y, pos1.z); //use only on same level, ofc if (abs(pos1.dist2d(tile) - pos2.dist2d(tile)) < 1.5) { if (!(cbp->isVisible(tile) && cbp->getTile(tile)->blocked)) //if there's invisible or unblocked tile between, it's good return false; } } } return true; //if all are visible and blocked, we're at dead end } int howManyTilesWillBeDiscovered(int radious, int3 pos, crint3 dir) { return howManyTilesWillBeDiscovered(pos + dir, radious, cb.get()); } void getVisibleNeighbours(const std::vector &tiles, std::vector &out) { for(const int3 &tile : tiles) { foreach_neighbour(tile, [&](int3 neighbour) { if(cb->isVisible(neighbour)) out.push_back(neighbour); }); } } ui64 howManyReinforcementsCanGet(HeroPtr 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(); } vcmi-0.98/AI/VCAI/AIUtility.h000066400000000000000000000112461250671757600154750ustar00rootroot00000000000000#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/Connection.h" #include "../../lib/CGameState.h" #include "../../lib/mapping/CMap.h" #include "../../lib/NetPacks.h" #include "../../lib/CStopWatch.h" /* * 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 * */ typedef const int3& crint3; typedef const std::string& crstring; const int HERO_GOLD_COST = 2500; 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 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 const CGHeroInstance *get(bool doWeExpectNull = false) const; bool validAndSet() const; template void serialize(Handler &h, const int version) { h & this->h & hid & 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; 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->traceStream() << boost::format("Time of %s was %d ms.") % txt % time.getDiff(); } }; struct AtScopeExit { std::function foo; AtScopeExit(const std::function &FOO) : foo(FOO) {} ~AtScopeExit() { foo(); } }; class ObjsVector : public std::vector { private: }; template bool objWithID(const CGObjectInstance *obj) { return obj->ID == id; } std::string strFromInt3(int3 pos); 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 int howManyTilesWillBeDiscovered(const int3 &pos, int radious, CCallback * cbp); int howManyTilesWillBeDiscovered(int radious, int3 pos, crint3 dir); void getVisibleNeighbours(const std::vector &tiles, std::vector &out); bool canBeEmbarkmentPoint(const TerrainTile *t, bool fromWater); bool isBlockedBorderGate(int3 tileToHit); 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 isSafeToVisit(HeroPtr h, crint3 tile); bool boundaryBetweenTwoPoints (int3 pos1, int3 pos2, CCallback * cbp); bool compareMovement(HeroPtr lhs, HeroPtr rhs); bool compareHeroStrength(HeroPtr h1, HeroPtr h2); bool compareArmyStrength(const CArmedInstance *a1, const CArmedInstance *a2); ui64 howManyReinforcementsCanGet(HeroPtr h, const CGTownInstance *t); int3 whereToExplore(HeroPtr h); class CDistanceSorter { const CGHeroInstance * hero; public: CDistanceSorter(const CGHeroInstance * hero): hero(hero) {} bool operator ()(const CGObjectInstance *lhs, const CGObjectInstance *rhs); }; vcmi-0.98/AI/VCAI/CMakeLists.txt000066400000000000000000000016301250671757600162030ustar00rootroot00000000000000project(VCAI) cmake_minimum_required(VERSION 2.6) if (FL_FOUND) include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib ${FL_INCLUDE_DIRS}) else() include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib ${CMAKE_HOME_DIRECTORY}/AI/FuzzyLite/fuzzylite) endif() set(VCAI_SRCS StdInc.cpp VCAI.cpp Goals.cpp AIUtility.cpp main.cpp Fuzzy.cpp ) add_library(VCAI SHARED ${VCAI_SRCS}) if (FL_FOUND) target_link_libraries(VCAI ${FL_LIBRARIES} vcmi) else() target_link_libraries(VCAI fl-static vcmi) endif() set_target_properties(VCAI PROPERTIES ${PCH_PROPERTIES}) cotire(VCAI) if (NOT APPLE) # Already inside vcmiclient bundle install(TARGETS VCAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR}) endif() vcmi-0.98/AI/VCAI/Fuzzy.cpp000066400000000000000000000422701250671757600153030ustar00rootroot00000000000000#include "StdInc.h" #include "Fuzzy.h" #include #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/CommonConstructors.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/VCMI_Lib.h" #include "../../CCallback.h" #include "VCAI.h" /* * Fuzzy.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us struct BankConfig; class IObjectInfo; class CBankInfo; class Engine; class InputVariable; class CGTownInstance; using namespace vstd; //using namespace Goals; FuzzyHelper *fh; extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; engineBase::engineBase() { engine.addRuleBlock(&rules); } void engineBase::configure() { engine.configure("Minimum", "Maximum", "Minimum", "AlgebraicSum", "Centroid"); logAi->infoStream() << 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; for(auto s : army->Slots()) { bool walker = true; if (s.second->type->hasBonusOfType(Bonus::SHOOTER)) { shootersStrenght += s.second->getPower(); walker = false; } if (s.second->type->hasBonusOfType(Bonus::FLYING)) { flyersStrenght += s.second->getPower(); walker = false; } if (walker) walkersStrenght += s.second->getPower(); amax (maxSpeed, s.second->type->valOfBonuses(Bonus::STACKS_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; } FuzzyHelper::FuzzyHelper() { initTacticalAdvantage(); ta.configure(); initVisitTile(); vt.configure(); } void FuzzyHelper::initTacticalAdvantage() { try { ta.ourShooters = new fl::InputVariable("OurShooters"); ta.ourWalkers = new fl::InputVariable("OurWalkers"); ta.ourFlyers = new fl::InputVariable("OurFlyers"); ta.enemyShooters = new fl::InputVariable("EnemyShooters"); ta.enemyWalkers = new fl::InputVariable("EnemyWalkers"); ta.enemyFlyers = new fl::InputVariable("EnemyFlyers"); //Tactical advantage calculation std::vector helper = { ta.ourShooters, ta.ourWalkers, ta.ourFlyers, ta.enemyShooters, ta.enemyWalkers, ta.enemyFlyers }; for (auto val : helper) { ta.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); } ta.ourSpeed = new fl::InputVariable("OurSpeed"); ta.enemySpeed = new fl::InputVariable("EnemySpeed"); helper = {ta.ourSpeed, ta.enemySpeed}; for (auto val : helper) { ta.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); } ta.castleWalls = new fl::InputVariable("CastleWalls"); ta.engine.addInputVariable(ta.castleWalls); { fl::Rectangle* none = new fl::Rectangle("NONE", CGTownInstance::NONE, CGTownInstance::NONE + (CGTownInstance::FORT - CGTownInstance::NONE) * 0.5f); ta.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); ta.castleWalls->addTerm(medium); fl::Ramp* high = new fl::Ramp("HIGH", CGTownInstance::CITADEL - 0.1, CGTownInstance::CASTLE); ta.castleWalls->addTerm(high); ta.castleWalls->setRange(CGTownInstance::NONE, CGTownInstance::CASTLE); } ta.bankPresent = new fl::InputVariable("Bank"); ta.engine.addInputVariable(ta.bankPresent); { fl::Rectangle* termFalse = new fl::Rectangle("FALSE", 0.0, 0.5f); ta.bankPresent->addTerm(termFalse); fl::Rectangle* termTrue = new fl::Rectangle("TRUE", 0.5f, 1); ta.bankPresent->addTerm(termTrue); ta.bankPresent->setRange(0, 1); } ta.threat = new fl::OutputVariable("Threat"); ta.engine.addOutputVariable(ta.threat); ta.threat->addTerm(new fl::Ramp("LOW", 1, MIN_AI_STRENGHT)); ta.threat->addTerm(new fl::Triangle("MEDIUM", 0.8, 1.2)); ta.threat->addTerm(new fl::Ramp("HIGH", 1, 1.5)); ta.threat->setRange(MIN_AI_STRENGHT, 1.5); ta.addRule("if OurShooters is MANY and EnemySpeed is LOW then Threat is LOW"); ta.addRule("if OurShooters is MANY and EnemyShooters is FEW then Threat is LOW"); ta.addRule("if OurSpeed is LOW and EnemyShooters is MANY then Threat is HIGH"); ta.addRule("if OurSpeed is HIGH and EnemyShooters is MANY then Threat is LOW"); ta.addRule("if OurWalkers is FEW and EnemyShooters is MANY then Threat is somewhat LOW"); ta.addRule("if OurShooters is MANY and EnemySpeed is HIGH then Threat is somewhat HIGH"); //just to cover all cases ta.addRule("if OurShooters is FEW and EnemySpeed is HIGH then Threat is MEDIUM"); ta.addRule("if EnemySpeed is MEDIUM then Threat is MEDIUM"); ta.addRule("if EnemySpeed is LOW and OurShooters is FEW then Threat is MEDIUM"); ta.addRule("if Bank is TRUE and OurShooters is MANY then Threat is somewhat HIGH"); ta.addRule("if Bank is TRUE and EnemyShooters is MANY then Threat is LOW"); ta.addRule("if CastleWalls is HIGH and OurWalkers is MANY then Threat is very HIGH"); ta.addRule("if CastleWalls is HIGH and OurFlyers is MANY and OurShooters is MANY then Threat is MEDIUM"); ta.addRule("if CastleWalls is MEDIUM and OurShooters is MANY and EnemyWalkers is MANY then Threat is LOW"); } catch (fl::Exception & pe) { logAi->errorStream() << "initTacticalAdvantage " << ": " << pe.getWhat(); } } 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 / totalChance; } float FuzzyHelper::getTacticalAdvantage (const CArmedInstance *we, const CArmedInstance *enemy) { float output = 1; try { armyStructure ourStructure = evaluateArmyStructure(we); armyStructure enemyStructure = evaluateArmyStructure(enemy); ta.ourWalkers->setInputValue(ourStructure.walkers); ta.ourShooters->setInputValue(ourStructure.shooters); ta.ourFlyers->setInputValue(ourStructure.flyers); ta.ourSpeed->setInputValue(ourStructure.maxSpeed); ta.enemyWalkers->setInputValue(enemyStructure.walkers); ta.enemyShooters->setInputValue(enemyStructure.shooters); ta.enemyFlyers->setInputValue(enemyStructure.flyers); ta.enemySpeed->setInputValue(enemyStructure.maxSpeed); bool bank = dynamic_cast (enemy); if (bank) ta.bankPresent->setInputValue(1); else ta.bankPresent->setInputValue(0); const CGTownInstance * fort = dynamic_cast (enemy); if (fort) { ta.castleWalls->setInputValue(fort->fortLevel()); } else ta.castleWalls->setInputValue(0); //engine.process(TACTICAL_ADVANTAGE);//TODO: Process only Tactical_Advantage ta.engine.process(); output = ta.threat->getOutputValue(); } catch (fl::Exception & fe) { logAi->errorStream() << "getTacticalAdvantage " << ": " << fe.getWhat(); } if (output < 0 || (output != output)) { fl::InputVariable* tab[] = {ta.bankPresent, ta.castleWalls, ta.ourWalkers, ta.ourShooters, ta.ourFlyers, ta.ourSpeed, ta.enemyWalkers, ta.enemyShooters, ta.enemyFlyers, ta.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]->getInputValue() << " "; logAi->errorStream() << log.str(); assert(false); } return output; } FuzzyHelper::TacticalAdvantage::~TacticalAdvantage() { //TODO: smart pointers? delete ourWalkers; delete ourShooters; delete ourFlyers; delete enemyWalkers; delete enemyShooters; delete enemyFlyers; delete ourSpeed; delete enemySpeed; delete bankPresent; delete castleWalls; delete threat; } //shared_ptr chooseSolution (std::vector> & vec) Goals::TSubgoal FuzzyHelper::chooseSolution (Goals::TGoalVec vec) { if (vec.empty()) //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; }; boost::sort (vec, compareGoals); return vec.back(); } float FuzzyHelper::evaluate (Goals::Explore & g) { return 1; } float FuzzyHelper::evaluate (Goals::RecruitHero & g) { return 1; //just try to recruit hero as one of options } FuzzyHelper::EvalVisitTile::~EvalVisitTile() { delete strengthRatio; delete heroStrength; delete turnDistance; delete missionImportance; } void FuzzyHelper::initVisitTile() { try { vt.strengthRatio = new fl::InputVariable("strengthRatio"); //hero must be strong enough to defeat guards vt.heroStrength = new fl::InputVariable("heroStrength"); //we want to use weakest possible hero vt.turnDistance = new fl::InputVariable("turnDistance"); //we want to use hero who is near vt.missionImportance = new fl::InputVariable("lockedMissionImportance"); //we may want to preempt hero with low-priority mission vt.value = new fl::OutputVariable("Value"); vt.value->setMinimum(0); vt.value->setMaximum(5); std::vector helper = {vt.strengthRatio, vt.heroStrength, vt.turnDistance, vt.missionImportance}; for (auto val : helper) { vt.engine.addInputVariable(val); } vt.engine.addOutputVariable(vt.value); vt.strengthRatio->addTerm(new fl::Ramp("LOW", SAFE_ATTACK_CONSTANT, 0)); vt.strengthRatio->addTerm(new fl::Ramp("HIGH", SAFE_ATTACK_CONSTANT, SAFE_ATTACK_CONSTANT * 3)); vt.strengthRatio->setRange(0, SAFE_ATTACK_CONSTANT * 3 ); //strength compared to our main hero vt.heroStrength->addTerm(new fl::Ramp("LOW", 0.2, 0)); vt.heroStrength->addTerm(new fl::Triangle("MEDIUM", 0.2, 0.8)); vt.heroStrength->addTerm(new fl::Ramp("HIGH", 0.5, 1)); vt.heroStrength->setRange(0.0, 1.0); vt.turnDistance->addTerm(new fl::Ramp("SMALL", 0.5, 0)); vt.turnDistance->addTerm(new fl::Triangle("MEDIUM", 0.1, 0.8)); vt.turnDistance->addTerm(new fl::Ramp("LONG", 0.5, 3)); vt.turnDistance->setRange(0.0, 3.0); vt.missionImportance->addTerm(new fl::Ramp("LOW", 2.5, 0)); vt.missionImportance->addTerm(new fl::Triangle("MEDIUM", 2, 3)); vt.missionImportance->addTerm(new fl::Ramp("HIGH", 2.5, 5)); vt.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 vt.value->addTerm(new fl::Ramp("LOW", 2.5, 0)); vt.value->addTerm(new fl::Triangle("MEDIUM", 2, 3)); //can't be center of mass :/ vt.value->addTerm(new fl::Ramp("HIGH", 2.5, 5)); vt.value->setRange(0.0,5.0); //use unarmed scouts if possible vt.addRule("if strengthRatio is HIGH and heroStrength is LOW then Value is very HIGH"); //we may want to use secondary hero(es) rather than main hero vt.addRule("if strengthRatio is HIGH and heroStrength is MEDIUM then Value is somewhat HIGH"); vt.addRule("if strengthRatio is HIGH and heroStrength is HIGH then Value is somewhat 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) vt.addRule("if strengthRatio is LOW and heroStrength is LOW then Value is very LOW"); //attempt to arm secondary heroes is not stupid vt.addRule("if strengthRatio is LOW and heroStrength is MEDIUM then Value is somewhat HIGH"); vt.addRule("if strengthRatio is LOW and heroStrength is HIGH then Value is LOW"); //do not cancel important goals vt.addRule("if lockedMissionImportance is HIGH then Value is very LOW"); vt.addRule("if lockedMissionImportance is MEDIUM then Value is somewhat LOW"); vt.addRule("if lockedMissionImportance is LOW then Value is HIGH"); //pick nearby objects if it's easy, avoid long walks vt.addRule("if turnDistance is SMALL then Value is HIGH"); vt.addRule("if turnDistance is MEDIUM then Value is MEDIUM"); vt.addRule("if turnDistance is LONG then Value is LOW"); } catch (fl::Exception & fe) { logAi->errorStream() << "visitTile " << ": " << fe.getWhat(); } } float FuzzyHelper::evaluate (Goals::VisitTile & g) { //we assume that hero is already set and we want to choose most suitable one for the mission if (!g.hero) return 0; //assert(cb->isInTheMap(g.tile)); float turns = 0; float distance = cb->getMovementCost(g.hero.h, g.tile); if (!distance) //we stand on that tile turns = 0; else { if (distance < g.hero->movement) //we can move there within one turn turns = (fl::scalar)distance / g.hero->movement; else turns = 1 + (fl::scalar)(distance - g.hero->movement) / g.hero->maxMovePoints(true); //bool on land? } float missionImportance = 0; if (vstd::contains(ai->lockedHeroes, g.hero)) missionImportance = ai->lockedHeroes[g.hero]->priority; float strengthRatio = 10.0f; //we are much stronger than enemy ui64 danger = evaluateDanger (g.tile, g.hero.h); if (danger) strengthRatio = (fl::scalar)g.hero.h->getTotalStrength() / danger; try { vt.strengthRatio->setInputValue(strengthRatio); vt.heroStrength->setInputValue((fl::scalar)g.hero->getTotalStrength() / ai->primaryHero()->getTotalStrength()); vt.turnDistance->setInputValue(turns); vt.missionImportance->setInputValue(missionImportance); vt.engine.process(); //engine.process(VISIT_TILE); //TODO: Process only Visit_Tile g.priority = vt.value->getOutputValue(); } catch (fl::Exception & fe) { logAi->errorStream() << "evaluate VisitTile " << ": " << fe.getWhat(); } assert (g.priority >= 0); return g.priority; } float FuzzyHelper::evaluate (Goals::VisitHero & g) { auto obj = cb->getObj(ObjectInstanceID(g.objid)); //we assume for now that these goals are similar if (!obj) return -100; //hero died in the meantime //TODO: consider direct copy (constructor?) g.setpriority(Goals::VisitTile(obj->visitablePos()).sethero(g.hero).setisAbstract(g.isAbstract).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(); return g.value / std::max(g.value - army, 1000.0f); } float FuzzyHelper::evaluate (Goals::ClearWayTo & g) { if (!g.hero.h) throw cannotFulfillGoalException("ClearWayTo called without hero!"); SectorMap sm(g.hero); int3 t = sm.firstTileToGet(g.hero, g.tile); if (t.valid()) { if (isSafeToVisit(g.hero, t)) { g.setpriority(Goals::VisitTile(g.tile).sethero(g.hero).setisAbstract(g.isAbstract).accept(this)); } else { g.setpriority (Goals::GatherArmy(evaluateDanger(t, g.hero.h)*SAFE_ATTACK_CONSTANT). sethero(g.hero).setisAbstract(true).accept(this)); } return g.priority; } else return -1; } float FuzzyHelper::evaluate (Goals::BuildThis & g) { return 1; } float FuzzyHelper::evaluate (Goals::DigAtTile & g) { return 0; } float FuzzyHelper::evaluate (Goals::CollectRes & g) { return 0; } float FuzzyHelper::evaluate (Goals::Build & g) { return 0; } float FuzzyHelper::evaluate (Goals::Invalid & g) { return -1e10; } float FuzzyHelper::evaluate (Goals::AbstractGoal & g) { logAi->warnStream() << boost::format("Cannot evaluate goal %s") % g.name(); return g.priority; } void FuzzyHelper::setPriority (Goals::TSubgoal & g) { g->setpriority(g->accept(this)); //this enforces returned value is set } vcmi-0.98/AI/VCAI/Fuzzy.h000066400000000000000000000044021250671757600147430ustar00rootroot00000000000000#pragma once #include "fl/Headers.h" #include "Goals.h" /* * Fuzzy.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ class VCAI; class CArmedInstance; class CBank; class engineBase { public: fl::Engine engine; fl::RuleBlock rules; engineBase(); void configure(); void addRule(const std::string &txt); }; class FuzzyHelper { friend class VCAI; class TacticalAdvantage : public engineBase { public: fl::InputVariable * ourWalkers, * ourShooters, * ourFlyers, * enemyWalkers, * enemyShooters, * enemyFlyers; fl::InputVariable * ourSpeed, * enemySpeed; fl::InputVariable * bankPresent; fl::InputVariable * castleWalls; fl::OutputVariable * threat; ~TacticalAdvantage(); } ta; class EvalVisitTile : public engineBase { public: fl::InputVariable * strengthRatio; fl::InputVariable * heroStrength; fl::InputVariable * turnDistance; fl::InputVariable * missionImportance; fl::OutputVariable * value; fl::RuleBlock rules; ~EvalVisitTile(); } vt; public: enum RuleBlocks {BANK_DANGER, TACTICAL_ADVANTAGE, VISIT_TILE}; //blocks should be initialized in this order, which may be confusing :/ FuzzyHelper(); void initTacticalAdvantage(); void initVisitTile(); float evaluate (Goals::Explore & g); float evaluate (Goals::RecruitHero & g); float evaluate (Goals::VisitTile & 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::GatherArmy & g); float evaluate (Goals::ClearWayTo & g); float evaluate (Goals::Invalid & g); float evaluate (Goals::AbstractGoal & g); void setPriority (Goals::TSubgoal & g); ui64 estimateBankDanger (const CBank * bank); float getTacticalAdvantage (const CArmedInstance *we, const CArmedInstance *enemy); //returns factor how many times enemy is stronger than us Goals::TSubgoal chooseSolution (Goals::TGoalVec vec); //shared_ptr chooseSolution (std::vector> & vec); }; vcmi-0.98/AI/VCAI/Goals.cpp000066400000000000000000000777231250671757600152340ustar00rootroot00000000000000#include "StdInc.h" #include "Goals.h" #include "VCAI.h" #include "Fuzzy.h" #include "../../lib/mapping/CMap.h" //for victory conditions /* * Goals.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; //TODO: this logic should be moved inside VCAI using namespace vstd; using namespace Goals; TSubgoal Goals::sptr(const AbstractGoal & tmp) { shared_ptr ptr; ptr.reset(tmp.clone()); return ptr; } std::string Goals::AbstractGoal::name() const //TODO: virtualize { std::string desc; switch (goalType) { case INVALID: return "INVALID"; case WIN: return "WIN"; case DO_NOT_LOSE: return "DO NOT LOOSE"; case CONQUER: return "CONQUER"; case BUILD: return "BUILD"; case EXPLORE: desc = "EXPLORE"; break; case GATHER_ARMY: desc = "GATHER 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"; break; case GATHER_TROOPS: desc = "GATHER TROOPS"; break; case GET_OBJ: { auto obj = cb->getObjInstance(ObjectInstanceID(objid)); if (obj) desc = "GET OBJ " + obj->getObjectName(); } 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 ISSUE_COMMAND: return "ISSUE COMMAND (unsupported)"; case VISIT_TILE: desc = "VISIT TILE " + tile(); break; case CLEAR_WAY_TO: desc = "CLEAR WAY TO " + tile(); break; case DIG_AT_TILE: desc = "DIG AT TILE " + tile(); 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; } //TODO: virtualize if code gets complex? bool Goals::AbstractGoal::operator== (AbstractGoal &g) { if (g.goalType != goalType) return false; if (g.isElementar != isElementar) //elementar goals fulfill long term non-elementar goals (VisitTile) return false; switch (goalType) { //no parameters case INVALID: case WIN: case DO_NOT_LOSE: case RECRUIT_HERO: //recruit any hero, as yet return true; break; //assigned to hero, no parameters case CONQUER: case EXPLORE: case GATHER_ARMY: //actual value is indifferent case BOOST_HERO: return g.hero.h == hero.h; //how comes HeroPtrs are equal for different heroes? break; //assigned hero and tile case VISIT_TILE: case CLEAR_WAY_TO: return (g.hero.h == hero.h && g.tile == tile); break; //assigned hero and object case GET_OBJ: case FIND_OBJ: //TODO: use subtype? case VISIT_HERO: case GET_ART_TYPE: case DIG_AT_TILE: return (g.hero.h == hero.h && g.objid == objid); break; //no check atm case COLLECT_RES: case GATHER_TROOPS: case ISSUE_COMMAND: case BUILD: //TODO: should be decomposed to build specific structures case BUILD_STRUCTURE: default: return false; } } //TODO: find out why the following are not generated automatically on MVS? namespace Goals { template <> void CGoal::accept (VCAI * ai) { ai->tryRealize(static_cast(*this)); } template <> void CGoal::accept (VCAI * ai) { ai->tryRealize(static_cast(*this)); } template <> float CGoal::accept (FuzzyHelper * f) { return f->evaluate(static_cast(*this)); } template <> float CGoal::accept (FuzzyHelper * f) { return f->evaluate(static_cast(*this)); } } //TSubgoal AbstractGoal::whatToDoToAchieve() //{ // logAi->debugStream() << boost::format("Decomposing goal of type %s") % name(); // return sptr (Goals::Explore()); //} 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 (Goals::GetArtOfType(goal.objectType)); case EventCondition::DESTROY: { if (goal.object) { return sptr (Goals::GetObj(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 (Goals::BuildThis(BuildingID::GRAIL, t)); } 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 (Goals::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 (Goals::DigAtTile(grailPos)); } //TODO: use FIND_OBJ else if(const CGObjectInstance * obj = ai->getUnvisitedObj(objWithID)) //there are unvisited Obelisks return sptr (Goals::GetObj(obj->id.getNum())); else return sptr (Goals::Explore()); } break; } case EventCondition::CONTROL: { if (goal.object) { return sptr (Goals::GetObj(goal.object->id.getNum())); } 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 (Goals::CollectRes(static_cast(goal.objectType), goal.value)); case EventCondition::HAVE_CREATURES: return sptr (Goals::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 (Goals::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; default: assert(0); } } return sptr (Goals::Invalid()); } 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 (Goals::GetObj(o->id.getNum())); else return sptr (Goals::Explore()); } std::string GetObj::completeMessage() const { return "hero " + hero.get()->name + " captured Object ID = " + boost::lexical_cast(objid); } TSubgoal GetObj::whatToDoToAchieve() { const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(objid)); if(!obj) return sptr (Goals::Explore()); if (obj->tempOwner == ai->playerID) //we can't capture our own object -> move to Win codition throw cannotFulfillGoalException("Cannot capture my own object " + obj->getObjectName()); int3 pos = obj->visitablePos(); if (hero) { if (ai->isAccessibleForHero(pos, hero)) return sptr (Goals::VisitTile(pos).sethero(hero)); } else { for (auto h : cb->getHeroesInfo()) { if (ai->isAccessibleForHero(pos, h)) return sptr(Goals::VisitTile(pos).sethero(h)); //we must visit object with same hero, if any } } return sptr (Goals::ClearWayTo(pos).sethero(hero)); } bool GetObj::fulfillsMe (TSubgoal goal) { if (goal->goalType == Goals::VISIT_TILE) { auto obj = cb->getObj(ObjectInstanceID(objid)); if (obj && obj->visitablePos() == goal->tile) //object could be removed return true; } return false; } 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 (Goals::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->errorStream() << "Hero " << hero.name << " tries to visit himself."; else { //can't use VISIT_TILE here as tile appears blocked by target hero //FIXME: elementar goal should not be abstract return sptr (Goals::VisitHero(objid).sethero(hero).settile(pos).setisElementar(true)); } } return sptr (Goals::Invalid()); } bool VisitHero::fulfillsMe (TSubgoal goal) { if (goal->goalType == Goals::VISIT_TILE && cb->getObj(ObjectInstanceID(objid))->visitablePos() == goal->tile) return true; else return false; } TSubgoal GetArtOfType::whatToDoToAchieve() { TSubgoal alternativeWay = CGoal::lookForArtSmart(aid); //TODO: use if(alternativeWay->invalid()) return sptr (Goals::FindObj(Obj::ARTIFACT, aid)); return sptr (Goals::Invalid()); } TSubgoal ClearWayTo::whatToDoToAchieve() { assert(cb->isInTheMap(tile)); //set tile if(!cb->isVisible(tile)) { logAi->errorStream() << "Clear way should be used with visible tiles!"; return sptr (Goals::Explore()); } return (fh->chooseSolution(getAllPossibleSubgoals())); } 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 SectorMap sm(h); int3 tileToHit = sm.firstTileToGet(h, tile); if (!tileToHit.valid()) continue; if (isBlockedBorderGate(tileToHit)) { //FIXME: this way we'll not visit gate and activate quest :? ret.push_back (sptr (Goals::FindObj (Obj::KEYMASTER, cb->getTile(tileToHit)->visitableObjects.back()->subID))); } auto topObj = cb->getTopObj(tileToHit); if (topObj) { if (vstd::contains(ai->reservedObjs, topObj) && !vstd::contains(ai->reservedHeroesMap[h], topObj)) { throw goalFulfilledException (sptr(Goals::ClearWayTo(tile, h))); continue; //do not capure object reserved by other hero } if (topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES) if (topObj != hero.get(true)) //the hero we want to free logAi->errorStream() << boost::format("%s stands in the way of %s") % topObj->getObjectName() % h->getObjectName(); if (topObj->ID == Obj::QUEST_GUARD || topObj->ID == Obj::BORDERGUARD) { if (shouldVisit(h, topObj)) { //do NOT use VISIT_TILE, as tile with quets guard can't be visited ret.push_back (sptr (Goals::GetObj(topObj->id.getNum()).sethero(h))); continue; //do not try to visit tile or gather army } else { //TODO: we should be able to return apriopriate quest here (VCAI::striveToQuest) logAi->debugStream() << "Quest guard blocks the way to " + tile(); } } } if (isSafeToVisit(h, tileToHit)) //this makes sense only if tile is guarded, but there i no quest object { ret.push_back (sptr (Goals::VisitTile(tileToHit).sethero(h))); } else { ret.push_back (sptr (Goals::GatherArmy(evaluateDanger(tileToHit, h)*SAFE_ATTACK_CONSTANT). sethero(h).setisAbstract(true))); } } if (ai->canRecruitAnyHero()) ret.push_back (sptr (Goals::RecruitHero())); if (ret.empty()) { logAi->warnStream() << "There is no known way to clear the way to tile " + tile(); throw goalFulfilledException (sptr(Goals::ClearWayTo(tile))); //make sure asigned hero gets unlocked } return ret; } std::string Explore::completeMessage() const { return "Hero " + hero.get()->name + " completed exploration"; } TSubgoal Explore::whatToDoToAchieve() { auto ret = fh->chooseSolution(getAllPossibleSubgoals()); if (hero) //use best step for this hero return ret; else { if (ret->hero.get(true)) return sptr (sethero(ret->hero.h).setisAbstract(true)); //choose this hero and then continue with him else return ret; //other solutions, like buying hero from tavern } } TGoalVec Explore::getAllPossibleSubgoals() { TGoalVec ret; std::vector heroes; if (hero) heroes.push_back(hero.h); else { //heroes = ai->getUnblockedHeroes(); heroes = cb->getHeroesInfo(); erase_if (heroes, [](const HeroPtr h) { if (ai->getGoal(h)->goalType == Goals::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) { SectorMap sm(h); for (auto obj : objs) //double loop, performance risk? { auto t = sm.firstTileToGet(h, obj->visitablePos()); //we assume that no more than one tile on the way is guarded if (ai->canReachTile(h, t)) ret.push_back (sptr(Goals::ClearWayTo(obj->visitablePos(), h).setisAbstract(true))); } int3 t = whereToExplore(h); if (t.valid()) { ret.push_back (sptr (Goals::VisitTile(t).sethero(h))); } else { ai->markHeroUnableToExplore (h); //there is no freely accessible tile, do not poll this hero anymore //possible issues when gathering army to break if (hero.h == h || (!hero && h == ai->primaryHero().h)) //check this only ONCE, high cost { t = ai->explorationDesperate(h); if (t.valid()) //don't waste time if we are completely blocked ret.push_back (sptr(Goals::ClearWayTo(t, h).setisAbstract(true))); } } } //we either don't have hero yet or none of heroes can explore if ((!hero || ret.empty()) && ai->canRecruitAnyHero()) ret.push_back (sptr(Goals::RecruitHero())); if (ret.empty()) { throw goalFulfilledException (sptr(Goals::Explore().sethero(hero))); } //throw cannotFulfillGoalException("Cannot explore - no possible ways found!"); return ret; } bool Explore::fulfillsMe (TSubgoal goal) { if (goal->goalType == Goals::EXPLORE) { if (goal->hero) return hero == goal->hero; else return true; //cancel ALL exploration } return false; } TSubgoal RecruitHero::whatToDoToAchieve() { const CGTownInstance *t = ai->findTownWithTavern(); if(!t) return sptr (Goals::BuildThis(BuildingID::TAVERN)); if(cb->getResourceAmount(Res::GOLD) < HERO_GOLD_COST) return sptr (Goals::CollectRes(Res::GOLD, HERO_GOLD_COST)); return iAmElementar(); } std::string VisitTile::completeMessage() const { return "Hero " + hero.get()->name + " visited tile " + tile(); } 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 (Goals::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(Goals::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(Goals::VisitTile(tile).sethero(h))); } if (ai->canRecruitAnyHero()) ret.push_back (sptr(Goals::RecruitHero())); } if (ret.empty()) { auto obj = 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(Goals::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(Goals::ClearWayTo(tile))); } //important - at least one sub-goal must handle case which is impossible to fulfill (unreachable tile) return ret; } TSubgoal DigAtTile::whatToDoToAchieve() { const CGObjectInstance *firstObj = 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 (Goals::VisitTile(tile)); } TSubgoal BuildThis::whatToDoToAchieve() { //TODO check res //look for town //prerequisites? return iAmElementar(); } TSubgoal CollectRes::whatToDoToAchieve() { std::vector markets; std::vector visObjs; ai->retreiveVisitableObjs(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) //TODO a moze po prostu test na pozwalanie handlu? 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 { return !(market->o->ID == Obj::TOWN && market->o->tempOwner == ai->playerID) && !ai->isAccessible(market->o->visitablePos()); }),markets.end()); if(!markets.size()) { for(const CGTownInstance *t : cb->getTownsInfo()) { if(cb->canBuildStructure(t, BuildingID::MARKETPLACE) == EBuildingState::ALLOWED) return sptr (Goals::BuildThis(BuildingID::MARKETPLACE, t)); } } 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 * (cb->getResourceAmount(i) / toGive); } if(howManyCanWeBuy + cb->getResourceAmount(static_cast(resID)) >= value) { auto backObj = cb->getTopObj(m->o->visitablePos()); //it'll be a hero if we have one there; otherwise marketplace assert(backObj); if (backObj->tempOwner != ai->playerID) { return sptr (Goals::GetObj(m->o->id.getNum())); } else { return sptr (Goals::GetObj(m->o->id.getNum()).setisElementar(true)); } } } return sptr (setisElementar(true)); //all the conditions for trade are met } TSubgoal GatherTroops::whatToDoToAchieve() { std::vector dwellings; for(const CGTownInstance *t : cb->getTownsInfo()) { 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)) //this assumes only creatures with dwellings are assigned to faction { dwellings.push_back(t); } else { return sptr (Goals::BuildThis(bid, t)); } } } for (auto obj : ai->visitableObjs) { if (obj->ID != Obj::CREATURE_GENERATOR1) //TODO: what with other creature generators? continue; auto d = dynamic_cast(obj); for (auto creature : d->creatures) { if (creature.first) //there are more than 0 creatures avaliabe { for (auto type : creature.second) { if (type == objid && ai->freeResources().canAfford(VLC->creh->creatures[type]->cost)) dwellings.push_back(d); } } } } if (dwellings.size()) { typedef std::map TDwellMap; // sorted helper auto comparator = [](const TDwellMap::value_type & a, const TDwellMap::value_type & b) -> bool { const CGPathNode *ln = ai->myCb->getPathsInfo(a.first)->getPathInfo(a.second->visitablePos()), *rn = ai->myCb->getPathsInfo(b.first)->getPathInfo(b.second->visitablePos()); if(ln->turns != rn->turns) return ln->turns < rn->turns; return (ln->moveRemains > rn->moveRemains); }; // for all owned heroes generate map nearest dwelling> TDwellMap nearestDwellings; for (const CGHeroInstance * hero : cb->getHeroesInfo(true)) { nearestDwellings[hero] = *boost::range::min_element(dwellings, CDistanceSorter(hero)); } // find hero who is nearest to a dwelling const CGDwelling * nearest = boost::range::min_element(nearestDwellings, comparator)->second; return sptr (Goals::GetObj(nearest->id.getNum())); } else return sptr (Goals::Explore()); //TODO: exchange troops between heroes } TSubgoal Conquer::whatToDoToAchieve() { 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()) { SectorMap sm(h); 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) //double loop, performance risk? { auto t = sm.firstTileToGet(h, obj->visitablePos()); //we assume that no more than one tile on the way is guarded if (ai->canReachTile(h, t)) ret.push_back (sptr(Goals::ClearWayTo(obj->visitablePos(), h).setisAbstract(true))); } } if (!objs.empty() && ai->canRecruitAnyHero()) //probably no point to recruit hero if we see no objects to capture ret.push_back (sptr(Goals::RecruitHero())); if (ret.empty()) ret.push_back (sptr(Goals::Explore())); //we need to find an enemy return ret; } TSubgoal Build::whatToDoToAchieve() { return iAmElementar(); } TSubgoal Invalid::whatToDoToAchieve() { return iAmElementar(); } 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; //TODO: include evaluation of monsters gather in calculation for (auto t : cb->getTownsInfo()) { auto pos = t->visitablePos(); if (ai->isAccessibleForHero(pos, hero)) { if(!t->visitingHero && howManyReinforcementsCanGet(hero,t)) { if (!vstd::contains (ai->townVisitsThisWeek[hero], t)) ret.push_back (sptr (Goals::VisitTile(pos).sethero(hero))); } auto bid = ai->canBuildAnyStructure(t, std::vector (unitsSource, unitsSource + ARRAY_COUNT(unitsSource)), 8 - cb->getDate(Date::DAY_OF_WEEK)); if (bid != BuildingID::NONE) ret.push_back (sptr(BuildThis(bid, t))); } } auto otherHeroes = cb->getHeroesInfo(); auto heroDummy = hero; erase_if(otherHeroes, [heroDummy](const CGHeroInstance * h) { return (h == heroDummy.h || !ai->isAccessibleForHero(heroDummy->visitablePos(), h, true) || !ai->canGetArmy(heroDummy.h, h) || ai->getGoal(h)->goalType == Goals::GATHER_ARMY); }); for (auto h : otherHeroes) { ret.push_back (sptr (Goals::VisitHero(h->id.getNum()).setisAbstract(true).sethero(hero))); //go to the other hero if we are faster ret.push_back (sptr (Goals::VisitHero(hero->id.getNum()).setisAbstract(true).sethero(h))); //let the other hero come to us } 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); for(auto & creLevel : dwelling->creatures) { if(creLevel.first) { for(auto & creatureID : creLevel.second) { auto creature = VLC->creh->creatures[creatureID]; if (ai->freeResources().canAfford(creature->cost)) objs.push_back(obj); } } } } } } for(auto h : cb->getHeroesInfo()) { SectorMap sm(h); for (auto obj : objs) { //find safe dwelling auto pos = obj->visitablePos(); if (ai->isGoodForVisit(obj, h, sm)) ret.push_back (sptr (Goals::VisitTile(pos).sethero(h))); } } if (ai->canRecruitAnyHero()) //this is not stupid in early phase of game ret.push_back (sptr(Goals::RecruitHero())); if (ret.empty()) { if (hero == ai->primaryHero() || value >= 1.1f) ret.push_back (sptr(Goals::Explore())); else //workaround to break loop - seemingly there are no ways to explore left throw goalFulfilledException (sptr(Goals::GatherArmy(0).sethero(hero))); } return ret; } //TSubgoal AbstractGoal::whatToDoToAchieve() //{ // logAi->debugStream() << boost::format("Decomposing goal of type %s") % name(); // return sptr (Goals::Explore()); //} TSubgoal AbstractGoal::goVisitOrLookFor(const CGObjectInstance *obj) { if(obj) return sptr (Goals::GetObj(obj->id.getNum())); else return sptr (Goals::Explore()); } TSubgoal AbstractGoal::lookForArtSmart(int aid) { return sptr (Goals::Invalid()); } bool AbstractGoal::invalid() const { return goalType == INVALID; } void AbstractGoal::accept (VCAI * ai) { ai->tryRealize(*this); } template void CGoal::accept (VCAI * ai) { ai->tryRealize(static_cast(*this)); //casting enforces template instantiation } float AbstractGoal::accept (FuzzyHelper * f) { return f->evaluate(*this); } template float CGoal::accept (FuzzyHelper * f) { return f->evaluate(static_cast(*this)); //casting enforces template instantiation } vcmi-0.98/AI/VCAI/Goals.h000066400000000000000000000265501250671757600146710ustar00rootroot00000000000000#pragma once #include "../../lib/VCMI_Lib.h" #include "../../lib/CBuildingHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CTownHandler.h" #include "AIUtility.h" /* * 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 * */ struct HeroPtr; class VCAI; class FuzzyHelper; namespace Goals { struct AbstractGoal; class VisitTile; typedef std::shared_ptr TSubgoal; typedef std::vector TGoalVec; enum EGoals { INVALID = -1, WIN, DO_NOT_LOSE, 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 OBJECT_GOALS_BEGIN, GET_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, //BUILD_STRUCTURE, ISSUE_COMMAND, VISIT_TILE, //tile, in conjunction with hero elementar; assumes tile is reachable CLEAR_WAY_TO, DIG_AT_TILE //elementar with hero on tile }; //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}; TSubgoal sptr(const AbstractGoal & tmp); class 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) AbstractGoal (EGoals goal = INVALID) : goalType (goal) { priority = 0; isElementar = false; isAbstract = false; value = 0; aid = -1; resID = -1; tile = int3(-1, -1, -1); town = nullptr; } 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() {TGoalVec vec; return vec;}; virtual TSubgoal whatToDoToAchieve() {return sptr(AbstractGoal());}; EGoals goalType; std::string name() const; virtual std::string completeMessage() const {return "This goal is unspecified!";}; bool invalid() const; static TSubgoal goVisitOrLookFor(const CGObjectInstance *obj); //if obj is nullptr, then we'll explore static TSubgoal lookForArtSmart(int aid); //checks non-standard ways of obtaining art (merchants, quests, etc.) static TSubgoal tryRecruitHero(); ///Visitor pattern //TODO: make accept work for shared_ptr... somehow virtual void accept (VCAI * ai); //unhandled goal will report standard error virtual float accept (FuzzyHelper * f); virtual bool operator== (AbstractGoal &g); virtual bool fulfillsMe (Goals::TSubgoal goal) //TODO: multimethod instead of type check { return false; } template void serialize(Handler &h, const int version) { h & goalType & isElementar & isAbstract & priority; h & value & resID & objid & aid & tile & hero & town & bid; } }; template class CGoal : public AbstractGoal { public: CGoal (EGoals goal = INVALID) : AbstractGoal (goal) { priority = 0; isElementar = false; isAbstract = false; value = 0; aid = -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; float accept (FuzzyHelper * f) override; CGoal * clone() const override { return new T(static_cast(*this)); //casting enforces template instantiation } TSubgoal iAmElementar() { setisElementar(true); shared_ptr ptr; ptr.reset(clone()); 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; } }; class Invalid : public CGoal { public: Invalid() : CGoal (Goals::INVALID) {priority = -1e10;}; TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; TSubgoal whatToDoToAchieve() override; }; class Win : public CGoal { public: Win() : CGoal (Goals::WIN) {priority = 100;}; TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; TSubgoal whatToDoToAchieve() override; }; class NotLose : public CGoal { public: NotLose() : CGoal (Goals::DO_NOT_LOSE) {priority = 100;}; TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; //TSubgoal whatToDoToAchieve() override; }; class Conquer : public CGoal { public: Conquer() : CGoal (Goals::CONQUER) {priority = 10;}; TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; }; class Build : public CGoal { public: Build() : CGoal (Goals::BUILD) {priority = 1;}; TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; TSubgoal whatToDoToAchieve() override; }; class Explore : public CGoal { public: Explore() : CGoal (Goals::EXPLORE){priority = 1;}; 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; }; class 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; }; class BoostHero : public CGoal { public: BoostHero() : CGoal (Goals::INVALID){priority = -1e10;}; //TODO TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; //TSubgoal whatToDoToAchieve() override {return sptr(Invalid());}; }; class RecruitHero : public CGoal { public: RecruitHero() : CGoal (Goals::RECRUIT_HERO){priority = 1;}; TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; TSubgoal whatToDoToAchieve() override; }; class BuildThis : public CGoal { public: BuildThis() : CGoal (Goals::BUILD_STRUCTURE){}; //FIXME: should be not allowed (private) BuildThis(BuildingID Bid, const CGTownInstance *tid) : CGoal (Goals::BUILD_STRUCTURE) {bid = Bid; town = tid; priority = 5;}; BuildThis(BuildingID Bid) : CGoal (Goals::BUILD_STRUCTURE) {bid = Bid; priority = 5;}; TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; TSubgoal whatToDoToAchieve() override; }; class 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 {return TGoalVec();}; TSubgoal whatToDoToAchieve() override; }; class 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 {return TGoalVec();}; TSubgoal whatToDoToAchieve() override; }; class GetObj : public CGoal { public: GetObj() {}; // empty constructor not allowed GetObj(int Objid) : CGoal(Goals::GET_OBJ) {objid = Objid; priority = 3;}; TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; TSubgoal whatToDoToAchieve() override; bool operator== (GetObj &g) {return g.objid == objid;} bool fulfillsMe (TSubgoal goal) override; std::string completeMessage() const override; }; class FindObj : public CGoal { public: FindObj() {}; // empty constructor not allowed FindObj(int ID) : CGoal(Goals::FIND_OBJ) {objid = ID; 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; }; class 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 operator== (VisitHero &g) {return g.objid == objid;} bool fulfillsMe (TSubgoal goal) override; std::string completeMessage() const override; }; class 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; }; class 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; //bool operator== (VisitTile &g) {return g.tile == tile;} std::string completeMessage() const override; }; class 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 operator== (ClearWayTo &g) {return g.tile == tile;} }; class 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; bool operator== (DigAtTile &g) {return g.tile == tile;} }; class CIssueCommand : public CGoal { std::function command; public: CIssueCommand(): CGoal(ISSUE_COMMAND){}; CIssueCommand(std::function _command): CGoal(ISSUE_COMMAND), command(_command) {priority = 1e10;}; TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; //TSubgoal whatToDoToAchieve() override {return sptr(Invalid());}; }; } vcmi-0.98/AI/VCAI/StdInc.cpp000066400000000000000000000000251250671757600153300ustar00rootroot00000000000000#include "StdInc.h" vcmi-0.98/AI/VCAI/StdInc.h000066400000000000000000000000521250671757600147750ustar00rootroot00000000000000#pragma once #include "../../Global.h" vcmi-0.98/AI/VCAI/VCAI.cbp000066400000000000000000000073021250671757600146550ustar00rootroot00000000000000 vcmi-0.98/AI/VCAI/VCAI.cpp000066400000000000000000002711551250671757600147040ustar00rootroot00000000000000#include "StdInc.h" #include "VCAI.h" #include "Goals.h" #include "Fuzzy.h" #include "../../lib/UnlockGuard.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/CModHandler.h" /* * CCreatureHandler.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ extern FuzzyHelper *fh; class CGVisitableOPW; const double SAFE_ATTACK_CONSTANT = 1.5; const int GOLD_RESERVE = 10000; //when buying creatures we want to keep at least this much gold (10000 so at least we'll be able to reach capitol) using namespace vstd; //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() { 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) unsigned char &retreiveTileN(std::vector< std::vector< std::vector > > &vectors, const int3 &pos) { return vectors[pos.x][pos.y][pos.z]; } const unsigned char &retreiveTileN(const std::vector< std::vector< std::vector > > &vectors, const int3 &pos) { return vectors[pos.x][pos.y][pos.z]; } void foreach_tile(std::vector< std::vector< std::vector > > &vectors, std::function foo) { for(auto & vector : vectors) for(auto j = vector.begin(); j != vector.end(); j++) for(auto & elem : *j) foo(elem); } struct ObjInfo { int3 pos; std::string name; ObjInfo(){} ObjInfo(const CGObjectInstance *obj): pos(obj->pos), name(obj->getObjectName()) { } }; std::map helperObjInfo; VCAI::VCAI(void) { LOG_TRACE(logAi); makingTurn = nullptr; destinationTeleport = ObjectInstanceID(); } VCAI::~VCAI(void) { LOG_TRACE(logAi); } 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 if(details.result == TryMoveHero::TELEPORTATION) { const int3 from = CGHeroInstance::convertPosition(details.start, false), to = CGHeroInstance::convertPosition(details.end, false); const CGObjectInstance *o1 = frontOrNull(cb->getVisitableObjs(from)), *o2 = frontOrNull(cb->getVisitableObjs(to)); 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->debugStream() << boost::format("Found a pair of subterranean gates between %s and %s!") % from % to; } } } } } void VCAI::stackChagedCount(const StackLocation &location, const TQuantity &change, bool isAbsolute) { LOG_TRACE_PARAMS(logAi, "isAbsolute '%i'", isAbsolute); NET_EVENT_HANDLER; } 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->debugStream() << boost::format("Player %d: I heard that player %d %s.") % playerID % player.getNum() % (victoryLossCheckResult.victory() ? "won" : "lost"); if(player == playerID) { if(victoryLossCheckResult.victory()) { logAi->debugStream() << "VCAI: I won! Incredible!"; logAi->debugStream() << "Turn nr " << myCb->getDate(); } else { logAi->debugStream() << "VCAI: Player " << player << " lost. It's me. What a disappointment! :("; } 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::stacksErased(const StackLocation &location) { 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) { markObjectVisited (visitedObj); unreserveObject(visitor, visitedObj); completeGoal (sptr(Goals::GetObj(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..) } status.heroVisit(visitedObj, start); } void VCAI::availableArtifactsChanged(const CGBlackMarket *bm /*= nullptr*/) { 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(); } 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); clearHeroesUnableToExplore(); } 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 and %s") % firstHero->name % secondHero->name)); 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; if (goalpriority1 > goalpriority2) pickBestCreatures (firstHero, secondHero); else if (goalpriority1 < goalpriority2) pickBestCreatures (secondHero, firstHero); else //regular criteria { if (firstHero->getFightingStrength() > secondHero->getFightingStrength() && canGetArmy (firstHero, secondHero)) pickBestCreatures (firstHero, secondHero); else if (canGetArmy (secondHero, firstHero)) pickBestCreatures (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()))); //TODO: exchange artifacts } 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::stackChangedType(const StackLocation &location, const CCreature &newType) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::stacksRebalanced(const StackLocation &src, const StackLocation &dst, TQuantity count) { LOG_TRACE(logAi); NET_EVENT_HANDLER; } void VCAI::newObject(const CGObjectInstance * obj) { LOG_TRACE(logAi); NET_EVENT_HANDLER; if(obj->isVisitable()) addVisitableObj(obj); clearHeroesUnableToExplore(); } void VCAI::objectRemoved(const CGObjectInstance *obj) { LOG_TRACE(logAi); NET_EVENT_HANDLER; erase_if_present(visitableObjs, obj); erase_if_present(alreadyVisited, obj); for (auto h : cb->getHeroesInfo()) unreserveObject(h, obj); //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::newStackInserted(const StackLocation &location, const CStackInstance &stack) { LOG_TRACE(logAi); 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(int type, int val) { LOG_TRACE_PARAMS(logAi, "type '%i', val '%i'", type % val); NET_EVENT_HANDLER; } void VCAI::stacksSwapped(const StackLocation &loc1, const StackLocation &loc2) { 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) { //we don't want to visit know object twice (do we really?) if(sop->val == playerID.getNum()) erase_if_present(visitableObjs, myCb->getObj(sop->id)); else 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); 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; } 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(shared_ptr CB) { LOG_TRACE(logAi); myCb = CB; cbc = CB; NET_EVENT_HANDLER; playerID = *myCb->getMyColor(); myCb->waitTillRealize = true; myCb->unlockGsWhenWaiting = true; if(!fh) fh = new FuzzyHelper(); retreiveVisitableObjs(); } 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, std::vector 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())); ObjectInstanceID choosenExit; if(impassable) knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE; else { if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, destinationTeleport)) choosenExit = destinationTeleport; if(!status.channelProbing()) { vstd::copy_if(exits, vstd::set_inserter(teleportChannelProbingList), [&](ObjectInstanceID id) -> bool { return !(vstd::contains(visitableObjs, cb->getObj(id)) || id == choosenExit); }); } } requestActionASAP([=]() { answerQuery(askID, choosenExit.getNum()); }); } 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([=]() { pickBestCreatures (down, up); answerQuery(queryID, 0); }); } void VCAI::saveGame(COSer & h, const int version) { LOG_TRACE_PARAMS(logAi, "version '%i'", version); NET_EVENT_HANDLER; validateVisitableObjs(); registerGoals(h); CAdventureAI::saveGame(h, version); serializeInternal(h, version); } void VCAI::loadGame(CISer & h, const int version) { LOG_TRACE_PARAMS(logAi, "version '%i'", version); NET_EVENT_HANDLER; registerGoals(h); 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; boost::shared_lock gsLock(cb->getGsMutex()); setThreadName("VCAI::makeTurn"); logGlobal->infoStream() << boost::format("Player %d starting turn") % static_cast(playerID.getNum()); switch(cb->getDate(Date::DAY_OF_WEEK)) { case 1: { townVisitsThisWeek.clear(); std::vector objs; retreiveVisitableObjs(objs, true); for(const CGObjectInstance *obj : objs) { if (isWeeklyRevisitable(obj)) { addVisitableObj(obj); erase_if_present (alreadyVisited, obj); } } } break; } markHeroAbleToExplore (primaryHero()); makeTurnInternal(); makingTurn.reset(); return; } void VCAI::makeTurnInternal() { saving = 0; //it looks messy here, but it's better to have armed heroes before attempting realizing goals for(const CGTownInstance *t : cb->getTownsInfo()) moveCreaturesToHero(t); try { //Pick objects reserved in previous turn - we expect only nerby objects there auto reservedHeroesCopy = reservedHeroesMap; //work on copy => the map may be changed while iterating (eg because hero died when attempting a goal) for (auto hero : reservedHeroesCopy) { if(reservedHeroesMap.count(hero.first)) continue; //hero might have been removed while we were in this loop if(!hero.first.validAndSet()) { logAi->errorStream() << "Hero " << hero.first.name << " present on reserved map. Shouldn't be. "; continue; } std::vector vec(hero.second.begin(), hero.second.end()); boost::sort (vec, CDistanceSorter(hero.first.get())); for (auto obj : vec) { if(!obj || !cb->getObj(obj->id)) { logAi->errorStream() << "Error: there is wrong object on list for hero " << hero.first->name; continue; } striveToGoal (sptr(Goals::VisitTile(obj->visitablePos()).sethero(hero.first))); } } //now try to win striveToGoal(sptr(Goals::Win())); //finally, continue our abstract long-term goals int oldMovement = 0; int newMovement = 0; while (true) { oldMovement = newMovement; //remember old value newMovement = 0; std::vector > safeCopy; for (auto mission : lockedHeroes) { fh->setPriority (mission.second); //re-evaluate if (canAct(mission.first)) { newMovement += mission.first->movement; safeCopy.push_back (mission); } } if (newMovement == oldMovement) //means our heroes didn't move or didn't re-assign their goals { logAi->warnStream() << "Our heroes don't move anymore, exhaustive decomposition failed"; break; } if (safeCopy.empty()) break; //all heroes exhausted their locked goals else { typedef std::pair TItrType; auto lockedHeroesSorter = [](TItrType m1, TItrType m2) -> bool { return m1.second->priority < m2.second->priority; }; boost::sort(safeCopy, lockedHeroesSorter); striveToGoal (safeCopy.back().second); } } auto quests = myCb->getMyQuests(); for (auto quest : quests) { striveToQuest (quest); } striveToGoal(sptr(Goals::Build())); //TODO: smarter building management performTypicalActions(); //for debug purpose for (auto h : cb->getHeroesInfo()) { if (h->movement) logAi->warnStream() << boost::format("hero %s has %d MP left") % h->name % h->movement; } } catch(boost::thread_interrupted &e) { logAi->debugStream() << "Making turn thread has been interrupted. We'll end without calling endTurn."; return; } catch(std::exception &e) { logAi->debugStream() << "Making turn thread has caught an exception: " << e.what(); } endTurn(); } bool VCAI::goVisitObj(const CGObjectInstance * obj, HeroPtr h) { int3 dst = obj->visitablePos(); logAi->debugStream() << boost::format("%s will try to visit %s at (%s)") % h->name % obj->getObjectName() % strFromInt3(dst); return moveHeroToTile(dst, h); } 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); 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() && cb->getResourceAmount(Res::GOLD) >= GameConstants::SPELLBOOK_GOLD_COST + saving[Res::GOLD] && h->visitedTown->hasBuilt (BuildingID::MAGES_GUILD_1)) cb->buyArtifact(h.get(), ArtifactID::SPELLBOOK); } break; } completeGoal (sptr(Goals::GetObj(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->errorStream() << "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->Slots().size() == 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->Slots().size() == 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::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter) { 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(); // const CCreature *c = VLC->creh->creatures[creID]; // if(containsSavedRes(c->cost)) // continue; amin(count, freeResources() / VLC->creh->creatures[creID]->cost); if(count > 0) cb->recruitCreatures(d, recruiter, creID, count, i); } } bool VCAI::tryBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays) { if (maxDays == 0) { logAi->warnStream() << "Request to build building " << building << " in 0 days!"; 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; TResources currentRes = cb->getResourceAmount(); //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) { if(!containsSavedRes(b->resources)) { logAi->debugStream() << boost::format("Player %d will build %s in town of %s at %s") % playerID % b->Name() % t->name % t->pos; cb->buildBuilding(t, buildID); return true; } continue; } else if(canBuild == EBuildingState::NO_RESOURCES) { //TResources income = estimateIncome(); TResources cost = t->town->buildings.at(buildID)->resources; for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) { //int diff = currentRes[i] - cost[i] + income[i]; int diff = currentRes[i] - cost[i]; if(diff < 0) saving[i] = 1; } continue; } else if (canBuild == EBuildingState::PREREQUIRES) { // can happen when dependencies have their own missing dependencies if (tryBuildStructure(t, buildID, maxDays - 1)) return true; } else if (canBuild == EBuildingState::MISSING_BASE) { if (tryBuildStructure(t, b->upgrade, maxDays - 1)) return true; } } return false; } //bool VCAI::canBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays=7) //{ // if (maxDays == 0) // { // logAi->warnStream() << "Request to build building " << building << " in 0 days!"; // 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; // // TResources currentRes = cb->getResourceAmount(); // TResources income = estimateIncome(); // //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) // { // if(!containsSavedRes(b->resources)) // { // logAi->debugStream() << boost::format("Player %d will build %s in town of %s at %s") % playerID % b->Name() % t->name % t->pos; // return true; // } // continue; // } // else if(canBuild == EBuildingState::NO_RESOURCES) // { // TResources cost = t->town->buildings.at(buildID)->resources; // for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) // { // int diff = currentRes[i] - cost[i] + income[i]; // if(diff < 0) // saving[i] = 1; // } // continue; // } // else if (canBuild == EBuildingState::PREREQUIRES) // { // // can happen when dependencies have their own missing dependencies // if (canBuildStructure(t, buildID, maxDays - 1)) // return true; // } // else if (canBuild == EBuildingState::MISSING_BASE) // { // if (canBuildStructure(t, b->upgrade, maxDays - 1)) // return true; // } // } // return false; //} bool VCAI::tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) { for(const auto & building : buildList) { if(t->hasBuilt(building)) continue; if (tryBuildStructure(t, building, maxDays)) return true; } return false; //Can't build anything } BuildingID VCAI::canBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) { for(const auto & building : buildList) { if(t->hasBuilt(building)) continue; if (cb->canBuildStructure(t, building)) return building; } return BuildingID::NONE; //Can't build anything } bool VCAI::tryBuildNextStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) { for(const auto & building : buildList) { if(t->hasBuilt(building)) continue; return tryBuildStructure(t, building, maxDays); } return false;//Nothing to build } void VCAI::buildStructure(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) TResources currentRes = cb->getResourceAmount(); TResources currentIncome = t->dailyIncome(); int townIncome = currentIncome[Res::GOLD]; if (tryBuildAnyStructure(t, std::vector(essential, essential + ARRAY_COUNT(essential)))) return; //we're running out of gold - try to build something gold-producing. Multiplier can be tweaked, 6 is minimum due to buildings costs if (currentRes[Res::GOLD] < townIncome * 6) if (tryBuildNextStructure(t, std::vector(goldSource, goldSource + ARRAY_COUNT(goldSource)))) return; if (cb->getDate(Date::DAY_OF_WEEK) > 6)// last 2 days of week - try to focus on growth { if (tryBuildNextStructure(t, std::vector(unitGrowth, unitGrowth + ARRAY_COUNT(unitGrowth)), 2)) return; } // first in-game week or second half of any week: try build dwellings if (cb->getDate(Date::DAY) < 7 || cb->getDate(Date::DAY_OF_WEEK) > 3) if (tryBuildAnyStructure(t, std::vector(unitsSource, unitsSource + ARRAY_COUNT(unitsSource)), 8 - cb->getDate(Date::DAY_OF_WEEK))) return; //try to upgrade dwelling for(int i = 0; i < ARRAY_COUNT(unitsUpgrade); i++) { if (t->hasBuilt(unitsSource[i]) && !t->hasBuilt(unitsUpgrade[i])) { if (tryBuildStructure(t, unitsUpgrade[i])) return; } } //remaining tasks if (tryBuildNextStructure(t, std::vector(goldSource, goldSource + ARRAY_COUNT(goldSource)))) return; if (tryBuildNextStructure(t, std::vector(spells, spells + ARRAY_COUNT(spells)))) return; if (tryBuildAnyStructure(t, std::vector(extra, extra + ARRAY_COUNT(extra)))) return; } bool VCAI::isGoodForVisit(const CGObjectInstance *obj, HeroPtr h, SectorMap &sm) { const int3 pos = obj->visitablePos(); if (canReachTile(h.get(), sm.firstTileToGet(h, obj->visitablePos())) && !obj->wasVisited(playerID) && (cb->getPlayerRelations(ai->playerID, obj->tempOwner) == PlayerRelations::ENEMIES || isWeeklyRevisitable(obj)) && //flag or get weekly resources / creatures isSafeToVisit(h, pos) && shouldVisit(h, obj) && !vstd::contains(alreadyVisited, obj) && !vstd::contains(reservedObjs, obj)) { 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 } return false; } std::vector VCAI::getPossibleDestinations(HeroPtr h) { validateVisitableObjs(); std::vector possibleDestinations; SectorMap sm(h); for(const CGObjectInstance *obj : visitableObjs) { if (isGoodForVisit(obj, h, sm)) { possibleDestinations.push_back(obj); } } boost::sort(possibleDestinations, CDistanceSorter(h.get())); return possibleDestinations; } bool VCAI::canReachTile (const CGHeroInstance * h, int3 t) { if (t.valid()) { auto obj = cb->getTopObj(t); if (obj && vstd::contains(ai->reservedObjs, obj) && !vstd::contains(reservedHeroesMap[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 cb->getResourceAmount(Res::GOLD) >= HERO_GOLD_COST && cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES && cb->getAvailableHeroes(t).size(); else return false; } void VCAI::wander(HeroPtr h) { //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, tmp; range::copy(reservedHeroesMap[h], std::back_inserter(tmp)); //also visit our reserved objects - but they are not prioritized to avoid running back and forth for (auto obj : tmp) { if (isAccessibleForHero (obj->visitablePos(), h)) //even nearby objects could be blocked by other heroes :( dests.push_back(obj); //can't use lambda for member function :( } range::copy(getPossibleDestinations(h), std::back_inserter(dests)); if(!dests.size()) { 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 { return howManyReinforcementsCanGet(h, lhs) < howManyReinforcementsCanGet(h, rhs); }; std::vector townsReachable; std::vector townsNotReachable; for(const CGTownInstance *t : cb->getTownsInfo()) { if(!t->visitingHero && howManyReinforcementsCanGet(h,t) && !vstd::contains(townVisitsThisWeek[h], t)) { if (isAccessibleForHero (t->visitablePos(), h)) townsReachable.push_back(t); else townsNotReachable.push_back(t); } } if(townsReachable.size()) { boost::sort(townsReachable, compareReinforcements); dests.push_back(townsReachable.back()); } else if(townsNotReachable.size()) { boost::sort(townsNotReachable, compareReinforcements); //TODO pick the truly best const CGTownInstance *t = townsNotReachable.back(); logAi->debugStream() << boost::format("%s can't reach any town, we'll try to make our way to %s at %s") % h->name % t->name % t->visitablePos(); int3 pos1 = h->pos; striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); //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) >= HERO_GOLD_COST) { std::vector towns = cb->getTownsInfo(); erase_if(towns, [](const CGTownInstance *t) -> bool { for(const CGHeroInstance *h : cb->getHeroesInfo()) if(!t->getArmyStrength() || howManyReinforcementsCanGet(h, t)) return true; return false; }); boost::sort(towns, compareArmyStrength); if(towns.size()) recruitHero(towns.back()); break; } else { logAi->debugStream() << "Nowhere more to go..."; break; } } //end of objs empty while (dests.size()) //performance improvement { //wander should not cause heroes to be reserved - they are always considered free const ObjectIdRef&dest = dests.front(); logAi->debugStream() << boost::format("Of all %d destinations, object oid=%d seems nice") % dests.size() % dest.id.getNum(); if(!goVisitObj(dest, h)) { if(!dest) { logAi->debugStream() << boost::format("Visit attempt made the object (id=%d) gone...") % dest.id.getNum(); } else { logAi->debugStream() << boost::format("Hero %s apparently used all MPs (%d left)") % h->name % h->movement; return; } } //TODO: refactor removing deleted objects from the list std::vector hlp; retreiveVisitableObjs(hlp, true); auto shouldBeErased = [&](const CGObjectInstance *obj) -> bool { if(!vstd::contains(hlp, obj)) { return true; } return false; }; erase_if(dests, shouldBeErased); erase_if_present(dests, dest); //why that fails sometimes when removing monsters? boost::sort(dests, CDistanceSorter(h.get())); //find next closest one } if (h->visitedTown) { townVisitsThisWeek[h].insert(h->visitedTown); buildArmyIn(h->visitedTown); } } } void VCAI::setGoal(HeroPtr h, Goals::TSubgoal goal) { //TODO: check for presence? if (goal->invalid()) erase_if_present(lockedHeroes, h); else { lockedHeroes[h] = goal; goal->setisElementar(false); //always evaluate goals before realizing } } void VCAI::completeGoal (Goals::TSubgoal goal) { logAi->traceStream() << boost::format("Completing goal: %s") % goal->name(); if (const CGHeroInstance * h = goal->hero.get(true)) { auto it = lockedHeroes.find(h); if (it != lockedHeroes.end()) if (it->second == goal) { logAi->debugStream() << boost::format("%s") % 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->debugStream() << boost::format("%s") % 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 = 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); 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->debugStream() << boost::format("Player %d: I %s the %s!") % playerID % (won ? "won" : "lost") % battlename; battlename.clear(); CAdventureAI::battleEnd(br); } void VCAI::waitTillFree() { auto unlock = vstd::makeUnlockSharedGuard(cb->getGsMutex()); status.waitTillFree(); } void VCAI::markObjectVisited (const CGObjectInstance *obj) { if(dynamic_cast(obj) || //we may want to wisit it with another hero dynamic_cast(obj) || //or another time (obj->ID == Obj::MONSTER)) return; alreadyVisited.insert(obj); } void VCAI::reserveObject(HeroPtr h, const CGObjectInstance *obj) { reservedObjs.insert(obj); reservedHeroesMap[h].insert(obj); logAi->debugStream() << "reserved object id=" << obj->id << "; address=" << (intptr_t)obj << "; name=" << obj->getObjectName(); } void VCAI::unreserveObject(HeroPtr h, const CGObjectInstance *obj) { erase_if_present(reservedObjs, obj); //unreserve objects erase_if_present(reservedHeroesMap[h], obj); } void VCAI::markHeroUnableToExplore (HeroPtr h) { heroesUnableToExplore.insert(h); } void VCAI::markHeroAbleToExplore (HeroPtr h) { erase_if_present(heroesUnableToExplore, h); } bool VCAI::isAbleToExplore (HeroPtr h) { return !vstd::contains (heroesUnableToExplore, h); } void VCAI::clearHeroesUnableToExplore() { heroesUnableToExplore.clear(); } void VCAI::validateVisitableObjs() { std::vector hlp; retreiveVisitableObjs(hlp, true); std::string errorMsg; auto shouldBeErased = [&](const CGObjectInstance *obj) -> bool { if(!vstd::contains(hlp, obj)) { logAi->errorStream() << helperObjInfo[obj].name << " at " << helperObjInfo[obj].pos << errorMsg; return true; } return false; }; //errorMsg is captured by ref so lambda will take the new text errorMsg = " shouldn't be on the visitable objects list!"; erase_if(visitableObjs, shouldBeErased); //FIXME: how comes our own heroes become inaccessible? 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 + "!"; erase_if(p.second, shouldBeErased); } errorMsg = " shouldn't be on the reserved objs list!"; 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!"; erase_if(alreadyVisited, shouldBeErased); } void VCAI::retreiveVisitableObjs(std::vector &out, bool includeOwned /*= false*/) 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::retreiveVisitableObjs() { 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; retreiveVisitableObjs(ret, true); erase_if(ret, [](const CGObjectInstance *obj) { return obj->tempOwner != ai->playerID; }); return ret; } void VCAI::addVisitableObj(const CGObjectInstance *obj) { visitableObjs.insert(obj); helperObjInfo[obj] = ObjInfo(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 == 5 && 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) { //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 /*= false*/) const { if (!includeAllies) { //don't visit tile occupied by allied hero for (auto obj : cb->getVisitableObjs(pos)) { if (obj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, obj->tempOwner) != PlayerRelations::ENEMIES && obj != h.get()) return false; } } return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable(); } bool VCAI::moveHeroToTile(int3 dst, HeroPtr h) { 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->debugStream() << boost::format("Moving hero %s to tile %s") % h->name % dst; 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(dst, path); if(path.nodes.empty()) { logAi->errorStream() << "Hero " << h->name << " cannot reach " << dst; throw goalFulfilledException (sptr(Goals::VisitTile(dst).sethero(h))); } auto getObj = [&](int3 coord, bool ignoreHero) { return cb->getTile(coord,false)->topVisitableObj(ignoreHero); }; auto doMovement = [&](int3 dst, bool transit) { cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true), transit); }; auto doTeleportMovement = [&](int3 dst, ObjectInstanceID exitId) { destinationTeleport = exitId; cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true)); destinationTeleport = ObjectInstanceID(); afterMovementCheck(); }; auto doChannelProbing = [&]() -> void { auto currentExit = getObj(CGHeroInstance::convertPosition(h->pos,false), false); assert(currentExit); status.setChannelProbing(true); for(auto exit : teleportChannelProbingList) doTeleportMovement(CGHeroInstance::convertPosition(h->pos,false), exit); teleportChannelProbingList.clear(); doTeleportMovement(CGHeroInstance::convertPosition(h->pos,false), currentExit->id); status.setChannelProbing(false); }; int i=path.nodes.size()-1; 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 nextObject = getObj(nextCoord, false); if(CGTeleport::isConnected(currentObject, nextObject)) { //we use special login if hero standing on teleporter it's mean we need doTeleportMovement(currentCoord, nextObject->id); if(teleportChannelProbingList.size()) doChannelProbing(); 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; if((i-2 >= 0) // Check there is node after next one; otherwise transit is pointless && (CGTeleport::isConnected(nextObject, getObj(path.nodes[i-2].coord, false)) || CGTeleport::isTeleport(nextObject))) { // Hero should be able to go through object if it's allow transit doMovement(endpos, true); } else doMovement(endpos, false); afterMovementCheck(); if(teleportChannelProbingList.size()) doChannelProbing(); } ret = !i; } if (h) { if (auto visitedObject = 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 if (!ret) //reserve object we are heading towards { auto obj = 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 { erase_if_present (lockedHeroes, h); //hero seemingly is confused throw cannotFulfillGoalException("Invalid path found!"); //FIXME: should never happen } logAi->debugStream() << boost::format("Hero %s moved from %s to %s. Returning %d.") % h->name % startHpos % h->visitablePos() % ret; } return ret; } void VCAI::tryRealize(Goals::Explore & g) { throw cannotFulfillGoalException("EXPLORE is not a 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? } } 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->warnStream() << boost::format("Why do I want to move hero %s to tile %s? Already standing on that tile! ") % g.hero->name % g.tile; throw goalFulfilledException (sptr(g)); } if (ai->moveHeroToTile(g.tile, 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) { const CGTownInstance *t = g.town; if(!t && g.hero) t = g.hero->visitedTown; if(!t) { for(const CGTownInstance *t : cb->getTownsInfo()) { switch(cb->canBuildStructure(t, BuildingID(g.bid))) { case EBuildingState::ALLOWED: cb->buildBuilding(t, BuildingID(g.bid)); return; default: break; } } } else if(cb->canBuildStructure(t, BuildingID(g.bid)) == EBuildingState::ALLOWED) { cb->buildBuilding(t, BuildingID(g.bid)); return; } 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() == CGHeroInstance::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::CollectRes & g) { if(cb->getResourceAmount(static_cast(g.resID)) >= g.value) throw cannotFulfillGoalException("Goal is already fulfilled!"); if(const CGObjectInstance *obj = cb->getObj(ObjectInstanceID(g.objid), false)) { if(const IMarket *m = IMarket::castFrom(obj, false)) { for (Res::ERes i = Res::WOOD; i <= Res::GOLD; vstd::advance(i, 1)) { if(i == g.resID) continue; int toGive, toGet; m->getOffer(i, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); toGive = toGive * (cb->getResourceAmount(i) / toGive); //TODO trade only as much as needed cb->trade(obj, EMarketMode::RESOURCE_RESOURCE, i, g.resID, toGive); if(cb->getResourceAmount(static_cast(g.resID)) >= g.value) return; } 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 { saving[g.resID] = 1; throw cannotFulfillGoalException("No object that could be used to raise resources!"); } } void VCAI::tryRealize(Goals::Build & g) { for(const CGTownInstance *t : cb->getTownsInfo()) { logAi->debugStream() << boost::format("Looking into %s") % t->name; buildStructure(t); buildArmyIn(t); if(!ai->primaryHero() || (t->getArmyStrength() > ai->primaryHero()->getArmyStrength() * 2 && !isAccessibleForHero(t->visitablePos(), ai->primaryHero()))) { recruitHero(t); buildArmyIn(t); } } throw cannotFulfillGoalException("BUILD has been realized as much as possible."); } void VCAI::tryRealize(Goals::Invalid & g) { throw cannotFulfillGoalException("I don't know how to fulfill this!"); } void VCAI::tryRealize(Goals::AbstractGoal & g) { logAi->debugStream() << boost::format("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(); boost::sort(hs, compareHeroStrength); if(hs.empty()) return nullptr; return hs.back(); } void VCAI::endTurn() { logAi->infoStream() << "Player " << static_cast(playerID.getNum()) << " ends turn"; if(!status.haveTurn()) { logAi->errorStream() << "Not having turn at the end of turn???"; } logAi->debugStream() << "Resources at the end of turn: " << cb->getResourceAmount(); 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->infoStream() << "Player " << static_cast(playerID.getNum()) << " ended turn"; } void VCAI::striveToGoal(Goals::TSubgoal ultimateGoal) { if (ultimateGoal->invalid()) return; //we are looking for abstract goals auto abstractGoal = striveToGoalInternal (ultimateGoal, false); if (abstractGoal->invalid()) return; //we received abstract goal, need to find concrete goals striveToGoalInternal (abstractGoal, true); //TODO: save abstract goals not related to hero } Goals::TSubgoal VCAI::striveToGoalInternal(Goals::TSubgoal ultimateGoal, bool onlyAbstract) { const int searchDepth = 30; const int searchDepth2 = searchDepth-2; Goals::TSubgoal abstractGoal = sptr(Goals::Invalid()); while(1) { Goals::TSubgoal goal = ultimateGoal; logAi->debugStream() << boost::format("Striving to goal of type %s") % ultimateGoal->name(); int maxGoals = searchDepth; //preventing deadlock for mutually dependent goals while(!goal->isElementar && maxGoals && (onlyAbstract || !goal->isAbstract)) { logAi->debugStream() << boost::format("Considering goal %s") % goal->name(); try { boost::this_thread::interruption_point(); goal = goal->whatToDoToAchieve(); --maxGoals; if (*goal == *ultimateGoal) //compare objects by value throw cannotFulfillGoalException("Goal dependency loop detected!"); } catch(goalFulfilledException &e) { //it is impossible to continue some goals (like exploration, for example) completeGoal (goal); logAi->debugStream() << boost::format("Goal %s decomposition failed: goal was completed as much as possible") % goal->name(); return sptr(Goals::Invalid()); } catch(std::exception &e) { logAi->debugStream() << boost::format("Goal %s decomposition failed: %s") % goal->name() % e.what(); return sptr(Goals::Invalid()); } } try { boost::this_thread::interruption_point(); if (!maxGoals) { std::runtime_error e("Too many subgoals, don't know what to do"); throw (e); } if (goal->hero) //lock this hero to fulfill ultimate goal { if (maxGoals) { setGoal(goal->hero, goal); } else { erase_if_present (lockedHeroes, goal->hero); // we seemingly don't know what to do with hero } } if (goal->isAbstract) { abstractGoal = goal; //allow only one abstract goal per call logAi->debugStream() << boost::format("Choosing abstract goal %s") % goal->name(); break; } else { logAi->debugStream() << boost::format("Trying to realize %s (value %2.3f)") % goal->name() % goal->priority; goal->accept(this); } boost::this_thread::interruption_point(); } catch(boost::thread_interrupted &e) { logAi->debugStream() << boost::format("Player %d: Making turn thread received an interruption!") % playerID; throw; //rethrow, we want to truly end this thread } catch(goalFulfilledException &e) { //the goal was completed successfully completeGoal (goal); //completed goal was main goal //TODO: find better condition if (ultimateGoal->fulfillsMe(goal) || maxGoals > searchDepth2) return sptr(Goals::Invalid()); } catch(std::exception &e) { logAi->debugStream() << boost::format("Failed to realize subgoal of type %s (greater goal type was %s), I will stop.") % goal->name() % ultimateGoal->name(); logAi->debugStream() << boost::format("The error message was: %s") % e.what(); break; } } return abstractGoal; } void VCAI::striveToQuest (const QuestInfo &q) { if (q.quest->missionType && q.quest->progress != CQuest::COMPLETE) { MetaString ms; q.quest->getRolloverText(ms, false); logAi->debugStream() << boost::format("Trying to realize quest: %s") % ms.toString(); auto heroes = cb->getHeroesInfo(); switch (q.quest->missionType) { case CQuest::MISSION_ART: { for (auto hero : heroes) //TODO: remove duplicated code? { if (q.quest->checkQuest(hero)) { striveToGoal (sptr(Goals::GetObj(q.obj->id.getNum()).sethero(hero))); return; } } for (auto art : q.quest->m5arts) { striveToGoal (sptr(Goals::GetArtOfType(art))); //TODO: transport? } break; } case CQuest::MISSION_HERO: { //striveToGoal (CGoal(RECRUIT_HERO)); for (auto hero : heroes) { if (q.quest->checkQuest(hero)) { striveToGoal (sptr(Goals::GetObj(q.obj->id.getNum()).sethero(hero))); return; } } striveToGoal (sptr(Goals::FindObj(Obj::PRISON))); //rule of a thumb - quest heroes usually are locked in prisons //BNLOG ("Don't know how to recruit hero with id %d\n", q.quest->m13489val); break; } case CQuest::MISSION_ARMY: { for (auto hero : heroes) { if (q.quest->checkQuest(hero)) //very bad info - stacks can be split between multiple heroes :( { striveToGoal (sptr(Goals::GetObj(q.obj->id.getNum()).sethero(hero))); return; } } for (auto creature : q.quest->m6creatures) { striveToGoal (sptr(Goals::GatherTroops(creature.type->idNumber, creature.count))); } //TODO: exchange armies... oh my //BNLOG ("Don't know how to recruit %d of %s\n", (int)(creature.count) % creature.type->namePl); break; } case CQuest::MISSION_RESOURCES: { if (heroes.size()) { if (q.quest->checkQuest(heroes.front())) //it doesn't matter which hero it is { striveToGoal (sptr(Goals::GetObj(q.obj->id.getNum()))); } else { for (int i = 0; i < q.quest->m7resources.size(); ++i) { if (q.quest->m7resources[i]) striveToGoal (sptr(Goals::CollectRes(i, q.quest->m7resources[i]))); } } } else striveToGoal (sptr(Goals::RecruitHero())); //FIXME: checkQuest requires any hero belonging to player :( break; } case CQuest::MISSION_KILL_HERO: case CQuest::MISSION_KILL_CREATURE: { auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); if (obj) striveToGoal (sptr(Goals::GetObj(obj->id.getNum()))); else striveToGoal (sptr(Goals::GetObj(q.obj->id.getNum()))); //visit seer hut break; } case CQuest::MISSION_PRIMARY_STAT: { auto heroes = cb->getHeroesInfo(); for (auto hero : heroes) { if (q.quest->checkQuest(hero)) { striveToGoal (sptr(Goals::GetObj(q.obj->id.getNum()).sethero(hero))); return; } } for (int i = 0; i < q.quest->m2stats.size(); ++i) { logAi->debugStream() << boost::format("Don't know how to increase primary stat %d") % i; } break; } case CQuest::MISSION_LEVEL: { auto heroes = cb->getHeroesInfo(); for (auto hero : heroes) { if (q.quest->checkQuest(hero)) { striveToGoal (sptr(Goals::GetObj(q.obj->id.getNum()).sethero(hero))); //TODO: causes infinite loop :/ return; } } logAi->debugStream() << boost::format("Don't know how to reach hero level %d") % q.quest->m13489val; break; } case CQuest::MISSION_PLAYER: { if (playerID.getNum() != q.quest->m13489val) logAi->debugStream() << boost::format("Can't be player of color %d") % q.quest->m13489val; break; } case CQuest::MISSION_KEYMASTER: { striveToGoal (sptr(Goals::FindObj(Obj::KEYMASTER, q.obj->subID))); break; } } } } void VCAI::performTypicalActions() { for(auto h : getUnblockedHeroes()) { logAi->debugStream() << boost::format("Looking into %s, MP=%d") % h->name.c_str() % h->movement; makePossibleUpgrades(*h); try { wander(h); } catch(std::exception &e) { logAi->debugStream() << boost::format("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); } int3 VCAI::explorationBestNeighbour(int3 hpos, int radius, HeroPtr h) { std::map dstToRevealedTiles; for(crint3 dir : dirs) if(cb->isInTheMap(hpos+dir)) if (isSafeToVisit(h, hpos + dir) && isAccessibleForHero (hpos + dir, h)) dstToRevealedTiles[hpos + dir] = howManyTilesWillBeDiscovered(radius, hpos, dir); if (dstToRevealedTiles.empty()) //yes, it DID happen! throw cannotFulfillGoalException("No neighbour will bring new discoveries!"); auto best = dstToRevealedTiles.begin(); for (auto i = dstToRevealedTiles.begin(); i != dstToRevealedTiles.end(); i++) { const CGPathNode *pn = cb->getPathsInfo(h.get())->getPathInfo(i->first); //const TerrainTile *t = cb->getTile(i->first); if(best->second < i->second && pn->reachable() && pn->accessible == CGPathNode::ACCESSIBLE) best = i; } if(best->second) return best->first; throw cannotFulfillGoalException("No neighbour will bring new discoveries!"); } int3 VCAI::explorationNewPoint(HeroPtr h) { //logAi->debugStream() << "Looking for an another place for exploration..."; int radius = h->getSightRadious(); std::vector > tiles; //tiles[distance_to_fow] tiles.resize(radius); CCallback * cbp = cb.get(); foreach_tile_pos([&](const int3 &pos) { if(!cbp->isVisible(pos)) tiles[0].push_back(pos); }); float bestValue = 0; //discovered tile to node distance ratio int3 bestTile(-1,-1,-1); for (int i = 1; i < radius; i++) { getVisibleNeighbours(tiles[i-1], tiles[i]); removeDuplicates(tiles[i]); for(const int3 &tile : tiles[i]) { if (!cb->getPathsInfo(h.get())->getPathInfo(tile)->reachable()) //this will remove tiles that are guarded by monsters (or removable objects) continue; CGPath path; cb->getPathsInfo(h.get())->getPath(tile, path); float ourValue = (float)howManyTilesWillBeDiscovered(tile, radius, cbp) / (path.nodes.size() + 1); //+1 prevents erratic jumps if (ourValue > bestValue) //avoid costly checks of tiles that don't reveal much { if(isSafeToVisit(h, tile) && !isBlockedBorderGate(tile)) { bestTile = tile; bestValue = ourValue; } } } } return bestTile; } int3 VCAI::explorationDesperate(HeroPtr h) { //logAi->debugStream() << "Looking for an another place for exploration..."; SectorMap sm(h); int radius = h->getSightRadious(); std::vector > tiles; //tiles[distance_to_fow] tiles.resize(radius); CCallback * cbp = cb.get(); foreach_tile_pos([&](const int3 &pos) { if(!cbp->isVisible(pos)) tiles[0].push_back(pos); }); ui64 lowestDanger = -1; int3 bestTile(-1,-1,-1); for (int i = 1; i < radius; i++) { getVisibleNeighbours(tiles[i-1], tiles[i]); removeDuplicates(tiles[i]); for(const int3 &tile : tiles[i]) { if (cbp->getTile(tile)->blocked) //does it shorten the time? continue; if (!howManyTilesWillBeDiscovered(tile, radius, cbp)) //avoid costly checks of tiles that don't reveal much continue; auto t = sm.firstTileToGet(h, tile); if (t.valid()) { ui64 ourDanger = evaluateDanger(t, h.h); if (ourDanger < lowestDanger) { if(!isBlockedBorderGate(t)) { if (!ourDanger) //at least one safe place found return t; bestTile = t; lowestDanger = ourDanger; } } } } } return bestTile; } TResources VCAI::estimateIncome() const { TResources ret; for(const CGTownInstance *t : cb->getTownsInfo()) { ret += t->dailyIncome(); } for(const CGObjectInstance *obj : 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; } bool VCAI::containsSavedRes(const TResources &cost) const { for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) { if(saving[i] && cost[i]) return true; } return false; } 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->debugStream() << boost::format("Trying to recruit a hero in %s at %s") % t->name % t->visitablePos(); 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); } else if(throwing) throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); } void VCAI::finish() { if(makingTurn) makingTurn->interrupt(); } void VCAI::requestActionASAP(std::function whatToDo) { // static boost::mutex m; // boost::unique_lock mylock(m); boost::barrier b(2); boost::thread newThread([&b,this,whatToDo]() { setThreadName("VCAI::requestActionASAP::helper"); SET_GLOBAL_STATE(this); boost::shared_lock gsLock(cb->getGsMutex()); b.wait(); whatToDo(); }); b.wait(); } void VCAI::lostHero(HeroPtr h) { logAi->debugStream() << boost::format("I lost my hero %s. It's best to forget and move on.") % h.name; erase_if_present(lockedHeroes, h); for(auto obj : reservedHeroesMap[h]) { erase_if_present(reservedObjs, obj); //unreserve all objects for that hero } erase_if_present(reservedHeroesMap, h); } void VCAI::answerQuery(QueryID queryID, int selection) { logAi->debugStream() << boost::format("I'll answer the query %d giving the choice %d") % queryID % selection; if(queryID != QueryID(-1)) { cb->selectionMade(selection, queryID); } else { logAi->debugStream() << boost::format("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"]["neutralAI"].getType() == JsonNode::DATA_STRING) return settings["server"]["neutralAI"].String(); else return "StupidAI"; } 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) { erase_if(visitableObjs, matchesId); for(auto &p : reservedHeroesMap) erase_if(p.second, matchesId); erase_if(reservedObjs, matchesId); } } TResources VCAI::freeResources() const { TResources myRes = cb->getResourceAmount(); myRes[Res::GOLD] -= GOLD_RESERVE; vstd::amax(myRes[Res::GOLD], 0); return myRes; } 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) { boost::unique_lock lock(mx); if(ID == QueryID(-1)) { logAi->debugStream() << boost::format("The \"query\" has an id %d, it'll be ignored as non-query. Description: %s") % ID % description; return; } assert(!vstd::contains(remainingQueries, ID)); assert(ID.getNum() >= 0); remainingQueries[ID] = description; cv.notify_all(); logAi->debugStream() << boost::format("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->debugStream() << boost::format("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->debugStream() << boost::format("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->errorStream() << "Something went really wrong, failed to answer query " << query << ": " << 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); ongoingHeroMovement = ongoing; cv.notify_all(); } bool AIStatus::channelProbing() { return ongoingChannelProbing; } SectorMap::SectorMap() { update(); } SectorMap::SectorMap(HeroPtr h) { update(); makeParentBFS(h->visitablePos()); } bool markIfBlocked(ui8 &sec, crint3 pos, const TerrainTile *t) { if(t->blocked && !t->visitable) { sec = NOT_AVAILABLE; return true; } return false; } bool markIfBlocked(ui8 &sec, crint3 pos) { return markIfBlocked(sec, pos, cb->getTile(pos)); } void SectorMap::update() { clear(); int curSector = 3; //0 is invisible, 1 is not explored CCallback * cbp = cb.get(); //optimization foreach_tile_pos([&](crint3 pos) { if(retreiveTile(pos) == NOT_CHECKED) { if(!markIfBlocked(retreiveTile(pos), pos)) exploreNewSector(pos, curSector++, cbp); } }); valid = true; } void SectorMap::clear() { sector = cb->getVisibilityMap(); valid = false; } void SectorMap::exploreNewSector(crint3 pos, int num, CCallback * cbp) { Sector &s = infoOnSectors[num]; s.id = num; s.water = cbp->getTile(pos)->isWater(); std::queue toVisit; toVisit.push(pos); while(!toVisit.empty()) { int3 curPos = toVisit.front(); toVisit.pop(); ui8 &sec = retreiveTile(curPos); if(sec == NOT_CHECKED) { const TerrainTile *t = cbp->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(retreiveTile(neighPos) == NOT_CHECKED) { toVisit.push(neighPos); //parent[neighPos] = curPos; } const TerrainTile *nt = cbp->getTile(neighPos, false); if(nt && nt->isWater() != s.water && canBeEmbarkmentPoint(nt, s.water)) { s.embarkmentPoints.push_back(neighPos); } }); if(t->visitable) { auto obj = t->visitableObjects.front(); if (vstd::contains(ai->knownSubterraneanGates, obj)) { s.subterraneanGates.push_back (obj); } } } } } } 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; } } bool isWeeklyRevisitable (const CGObjectInstance * obj) { //TODO: allow polling of remaining creatures in dwelling if (dynamic_cast(obj) || //ensures future compatibility, unlike IDs dynamic_cast(obj) || 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 break; 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 } break; case Obj::BORDERGUARD: //open borderguard if possible 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 } break; 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: //TODO: mechanism for handling monoliths return false; case Obj::SCHOOL_OF_MAGIC: case Obj::SCHOOL_OF_WAR: { TResources myRes = ai->myCb->getResourceAmount(); if (myRes[Res::GOLD] - GOLD_RESERVE < 1000) return false; } break; case Obj::LIBRARY_OF_ENLIGHTENMENT: if (h->level < 12) return false; break; case Obj::TREE_OF_KNOWLEDGE: { TResources myRes = ai->myCb->getResourceAmount(); if (myRes[Res::GOLD] - GOLD_RESERVE < 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;// GameConstants::MAX_HEROES_PER_PLAYER; case Obj::BOAT: return false; //Boats are handled by pathfinder } if (obj->wasVisited(*h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player); return false; return true; } int3 SectorMap::firstTileToGet(HeroPtr h, crint3 dst) /* 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 ret(-1,-1,-1); int sourceSector = retreiveTile(h->visitablePos()), destinationSector = retreiveTile(dst); const Sector *src = &infoOnSectors[sourceSector], *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[retreiveTile(ep)]; //preds[s].push_back(neigh); if(!preds[neigh]) { preds[neigh] = s; sectorQueue.push(neigh); } } for (auto gate : s->subterraneanGates) { auto gatePair = ai->knownSubterraneanGates.find(gate); if (gatePair != ai->knownSubterraneanGates.end()) { //check the other side of gate Sector *neigh = &infoOnSectors[retreiveTile(gatePair->second->visitablePos())]; if(!preds[neigh]) //if we didn't come into this sector yet { preds[neigh] = s; //it becomes our new target sector 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 = cb->getTile(pos); return t && t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT && retreiveTile(pos) == sectorToReach->id; }); 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 || retreiveTile(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 { //auto t = findFirstVisitableTile (h, dst); //if (t.valid()) // return t; //TODO: pop sectors linked by Subterranean Gate in loop auto firstGate = boost::find_if(src->subterraneanGates, [=](const CGObjectInstance * gate) -> bool { //make sure no hero block the way auto pos = ai->knownSubterraneanGates[gate]->visitablePos(); const TerrainTile *t = cb->getTile(pos); return t && t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::SUBTERRANEAN_GATE && retreiveTile(pos) == sectorToReach->id; }); if(firstGate != src->subterraneanGates.end()) { //TODO: pahtfinder can find path through subterranean gates, but this function only reaches closest gate return (*firstGate)->visitablePos(); } //TODO //Monolith? Whirlpool? ... 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 { return findFirstVisitableTile(h, dst); } //FIXME: find out why this line is reached logAi->errorStream() << ("Impossible happened at SectorMap::firstTileToGet"); return ret; } 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) { logAi->warnStream() << ("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 = retreiveTile(source); std::queue toVisit; toVisit.push(source); while(!toVisit.empty()) { int3 curPos = toVisit.front(); toVisit.pop(); ui8 &sec = retreiveTile(curPos); assert(sec == mySector); //consider only tiles from the same sector UNUSED(sec); foreach_neighbour(curPos, [&](crint3 neighPos) { if(retreiveTile(neighPos) == mySector && !vstd::contains(parent, neighPos)) { if (cb->canMoveBetween(curPos, neighPos)) { toVisit.push(neighPos); parent[neighPos] = curPos; } } }); } } unsigned char & SectorMap::retreiveTile(crint3 pos) { return retreiveTileN(sector, pos); } vcmi-0.98/AI/VCAI/VCAI.h000066400000000000000000000454631250671757600143520ustar00rootroot00000000000000#pragma once #include "AIUtility.h" #include "Goals.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/spells/CSpellHandler.h" #include "../../lib/Connection.h" #include "../../lib/CGameState.h" #include "../../lib/mapping/CMap.h" #include "../../lib/NetPacks.h" #include "../../lib/CondSh.h" struct QuestInfo; /* * 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 * */ 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 & remainingQueries & requestToQueryID & havingTurn; } }; 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 subterraneanGates; bool water; //all tiles of sector are land or water Sector() { id = -1; } }; bool valid; //some kind of lazy eval std::map parent; std::vector>> sector; //std::vector>> pathfinderSector; std::map infoOnSectors; SectorMap(); SectorMap(HeroPtr h); void update(); void clear(); void exploreNewSector(crint3 pos, int num, CCallback * cbp); void write(crstring fname); unsigned char &retreiveTile(crint3 pos); 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); }; //Set of buildings for different goals. Does not include any prerequisites. const BuildingID essential[] = {BuildingID::TAVERN, BuildingID::TOWN_HALL}; const BuildingID goldSource[] = {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}; const BuildingID 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}; const BuildingID 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}; const BuildingID unitGrowth[] = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR}; const BuildingID spells[] = {BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5}; const BuildingID extra[] = {BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, BuildingID::SPECIAL_4, BuildingID::SHIPYARD}; // all remaining buildings class VCAI : public CAdventureAI { public: //internal methods for town development //try build an unbuilt structure in maxDays at most (0 = indefinite) /*bool canBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays=7);*/ bool tryBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays=7); //try build ANY unbuilt structure BuildingID canBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays=7); bool tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays=7); //try build first unbuilt structure bool tryBuildNextStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays=7); friend class FuzzyHelper; std::map > knownTeleportChannels; std::map knownSubterraneanGates; ObjectInstanceID destinationTeleport; 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; 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 TResources saving; AIStatus status; std::string battlename; shared_ptr myCb; unique_ptr makingTurn; VCAI(void); ~VCAI(void); //TODO: use only smart pointers? void tryRealize(Goals::Explore & g); void tryRealize(Goals::RecruitHero & g); void tryRealize(Goals::VisitTile & g); void tryRealize(Goals::VisitHero & g); void tryRealize(Goals::BuildThis & g); void tryRealize(Goals::DigAtTile & g); void tryRealize(Goals::CollectRes & g); void tryRealize(Goals::Build & g); void tryRealize(Goals::Invalid & g); void tryRealize(Goals::AbstractGoal & g); int3 explorationBestNeighbour(int3 hpos, int radius, HeroPtr h); int3 explorationNewPoint(HeroPtr h); int3 explorationDesperate(HeroPtr h); bool canReachTile (const CGHeroInstance * h, int3 t); void recruitHero(); virtual std::string getBattleAIName() const override; virtual void init(shared_ptr CB) override; virtual void yourTurn() override; virtual 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 virtual void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO virtual 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. virtual 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 virtual void showTeleportDialog(TeleportChannelID channel, std::vector exits, bool impassable, QueryID askID) override; virtual void saveGame(COSer & h, const int version) override; //saving virtual void loadGame(CISer & h, const int version) override; //loading virtual void finish() override; virtual void availableCreaturesChanged(const CGDwelling *town) override; virtual void heroMoved(const TryMoveHero & details) override; virtual void stackChagedCount(const StackLocation &location, const TQuantity &change, bool isAbsolute) override; virtual void heroInGarrisonChange(const CGTownInstance *town) override; virtual void centerView(int3 pos, int focusTime) override; virtual void tileHidden(const std::unordered_set &pos) override; virtual void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) override; virtual void artifactAssembled(const ArtifactLocation &al) override; virtual void showTavernWindow(const CGObjectInstance *townOrTavern) override; virtual void showThievesGuildWindow (const CGObjectInstance * obj) override; virtual void playerBlocked(int reason, bool start) override; virtual void showPuzzleMap() override; virtual void showShipyardDialog(const IShipyard *obj) override; virtual void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; virtual void artifactPut(const ArtifactLocation &al) override; virtual void artifactRemoved(const ArtifactLocation &al) override; virtual void stacksErased(const StackLocation &location) override; virtual void artifactDisassembled(const ArtifactLocation &al) override; virtual void heroVisit(const CGHeroInstance *visitor, const CGObjectInstance *visitedObj, bool start) override; virtual void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; virtual void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override; virtual void tileRevealed(const std::unordered_set &pos) override; virtual void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; virtual void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; virtual void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) override; virtual void heroMovePointsChanged(const CGHeroInstance * hero) override; virtual void stackChangedType(const StackLocation &location, const CCreature &newType) override; virtual void stacksRebalanced(const StackLocation &src, const StackLocation &dst, TQuantity count) override; virtual void newObject(const CGObjectInstance * obj) override; virtual void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override; virtual void playerBonusChanged(const Bonus &bonus, bool gain) override; virtual void newStackInserted(const StackLocation &location, const CStackInstance &stack) override; virtual void heroCreated(const CGHeroInstance*) override; virtual void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; virtual void showInfoDialog(const std::string &text, const std::vector &components, int soundID) override; virtual void requestRealized(PackageApplied *pa) override; virtual void receivedResource(int type, int val) override; virtual void stacksSwapped(const StackLocation &loc1, const StackLocation &loc2) override; virtual void objectRemoved(const CGObjectInstance *obj) override; virtual void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) override; virtual void heroManaPointsChanged(const CGHeroInstance * hero) override; virtual void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; virtual void battleResultsApplied() override; virtual void objectPropertyChanged(const SetObjectProperty * sop) override; virtual void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; virtual void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain) override; virtual void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) override; void showWorldViewEx(const std::vector & objectPositions) override; virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; virtual void battleEnd(const BattleResult *br) override; void makeTurn(); void makeTurnInternal(); void performTypicalActions(); void buildArmyIn(const CGTownInstance * t); void striveToGoal(Goals::TSubgoal ultimateGoal); Goals::TSubgoal striveToGoalInternal(Goals::TSubgoal ultimateGoal, bool onlyAbstract); void endTurn(); void wander(HeroPtr h); void setGoal(HeroPtr h, Goals::TSubgoal goal); void completeGoal (Goals::TSubgoal goal); //safely removes goal from reserved hero void striveToQuest (const QuestInfo &q); void recruitHero(const CGTownInstance * t, bool throwing = false); bool isGoodForVisit(const CGObjectInstance *obj, HeroPtr h, SectorMap &sm); std::vector getPossibleDestinations(HeroPtr h); void buildStructure(const CGTownInstance * t); //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 moveCreaturesToHero(const CGTownInstance * t); bool goVisitObj(const CGObjectInstance * obj, HeroPtr h); void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h); bool moveHeroToTile(int3 dst, HeroPtr h); 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 clearHeroesUnableToExplore(); 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 retreiveVisitableObjs(std::vector &out, bool includeOwned = false) const; void retreiveVisitableObjs(); std::vector getFlaggedObjects() const; const CGObjectInstance *lookForArt(int aid) const; bool isAccessible(const int3 &pos); HeroPtr getHeroWithGrail() const; const CGObjectInstance *getUnvisitedObj(const std::function &predicate); bool isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies = false) const; 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; TResources freeResources() const; //owned resources minus gold reserve TResources estimateIncome() const; bool containsSavedRes(const TResources &cost) 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); 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(); } template void serializeInternal(Handler &h, const int version) { h & knownTeleportChannels & knownSubterraneanGates & destinationTeleport; h & townVisitsThisWeek & lockedHeroes & reservedHeroesMap; //FIXME: cannot instantiate abstract class h & visitableObjs & alreadyVisited & reservedObjs; h & saving & status & 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 { public: Goals::TSubgoal goal; explicit goalFulfilledException(Goals::TSubgoal Goal) : goal(Goal) { } virtual ~goalFulfilledException() throw () { }; const char *what() const throw () override { return goal->name().c_str(); } }; void makePossibleUpgrades(const CArmedInstance *obj); vcmi-0.98/AI/VCAI/VCAI.vcxproj000066400000000000000000000203361250671757600156060ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 {276C3DB0-7A6B-4417-8E5C-322B08633AAC} StupidAI DynamicLibrary true MultiByte v120_xp DynamicLibrary true MultiByte v120_xp DynamicLibrary false true MultiByte v120_xp DynamicLibrary false true MultiByte v120_xp $(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) $(FUZZYLITEDIR) Use StdInc.h /Zm199 %(AdditionalOptions) VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) ..\..\..\libs;..\..;.. Use StdInc.h /Zm150 %(AdditionalOptions) VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) $(VCMI_Out);$(OutDir);%(AdditionalLibraryDirectories) Create Create Create Create vcmi-0.98/AI/VCAI/main.cpp000066400000000000000000000011331250671757600150710ustar00rootroot00000000000000#include "StdInc.h" #include "VCAI.h" #ifdef __GNUC__ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif #ifdef VCMI_ANDROID #define GetGlobalAiVersion VCAI_GetGlobalAiVersion #define GetAiName VCAI_GetAiName #define GetNewAI VCAI_GetNewAI #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(shared_ptr &out) { out = make_shared(); } vcmi-0.98/AUTHORS000066400000000000000000000035731250671757600135100ustar00rootroot00000000000000VCMI 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 suport, 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 vcmi-0.98/CCallback.cpp000066400000000000000000000232331250671757600147360ustar00rootroot00000000000000#include "StdInc.h" #include "CCallback.h" #include "lib/CCreatureHandler.h" #include "client/CGameInfo.h" #include "lib/CGameState.h" #include "lib/BattleState.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/Connection.h" #include "lib/NetPacks.h" #include "client/mapHandler.h" #include "lib/spells/CSpellHandler.h" #include "lib/CArtHandler.h" #include "lib/GameConstants.h" #ifdef min #undef min #endif #ifdef max #undef max #endif #include "lib/UnlockGuard.h" /* * 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 * */ template bool isType(CPack *pack) { return pack->getType() == N; } 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) { ASSERT_IF_CALLED_WITH_PLAYER if(queryID == QueryID(-1)) { logGlobal->errorStream() << "Cannot answer the query -1!"; return false; } QueryReply pack(queryID,selection); pack.player = *player; return sendRequest(&pack); } void CCallback::recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level/*=-1*/) { if(player!=obj->tempOwner && obj->ID != Obj::WAR_MACHINE_FACTORY) return; RecruitCreatures pack(obj->id, dst->id, ID, amount, level); sendRequest(&pack); } bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos) { if(((player>=0) && 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->traceStream() << "Player " << *player << " ended his turn."; EndTurn pack; sendRequest(&pack); //report that we ended turn } 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; } // int CCallback::getMySerial() const // { // boost::shared_lock lock(*gs->mx); // return gs->players[player].serial; // } 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(BattleAction* action) { assert(action->actionType == Battle::HERO_SPELL); MakeCustomAction mca(*action); sendRequest(&mca); return 0; } int CBattleCallback::sendRequest(const CPack *request) { int requestID = cl->sendRequest(request, *player); if(waitTillRealize) { logGlobal->traceStream() << boost::format("We'll wait till request %d is answered.\n") % requestID; auto gsUnlocker = vstd::makeUnlockSharedGuardIf(getGsMutex(), unlockGsWhenWaiting); cl->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, int id1, int id2, int val1, const CGHeroInstance *hero/* = nullptr*/) { TradeOnMarketplace pack; pack.market = market; pack.hero = hero; pack.mode = mode; pack.r1 = id1; pack.r2 = id2; pack.val = val1; sendRequest(&pack); } void CCallback::setFormation(const CGHeroInstance * hero, bool tight) { const_cast(hero)-> formation = 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(*player, mess, currentObject? currentObject->id : ObjectInstanceID(-1)); sendRequest(&(CPackForClient&)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(GS, Player, C) { waitTillRealize = false; unlockGsWhenWaiting = false; } CCallback::~CCallback() { //trivial, but required. Don`t remove. } bool CCallback::canMoveBetween(const int3 &a, const int3 &b) { //TODO: merge with Pathfinder::canMoveBetween return gs->checkForVisitableDir(a, b) && gs->checkForVisitableDir(b, a); } int CCallback::getMovementCost(const CGHeroInstance * hero, int3 dest) { return gs->getMovementCost(hero, hero->visitablePos(), dest, hero->hasBonusOfType (Bonus::FLYING_MOVEMENT), hero->movement); } const CPathsInfo * 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); } void CCallback::unregisterAllInterfaces() { cl->playerint.clear(); cl->battleints.clear(); } 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(shared_ptr gameEvents) { cl->additionalPlayerInts[*player].push_back(gameEvents); } void CCallback::registerBattleInterface(shared_ptr battleEvents) { cl->additionalBattleInts[*player].push_back(battleEvents); } void CCallback::unregisterGameInterface(shared_ptr gameEvents) { cl->additionalPlayerInts[*player] -= gameEvents; } void CCallback::unregisterBattleInterface(shared_ptr battleEvents) { cl->additionalBattleInts[*player] -= battleEvents; } CBattleCallback::CBattleCallback(CGameState *GS, boost::optional Player, CClient *C ) { gs = GS; 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.98/CCallback.h000066400000000000000000000205761250671757600144120ustar00rootroot00000000000000#pragma once #include "lib/CGameInfoCallback.h" #include "lib/int3.h" // for int3 /* * 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 * */ class CGHeroInstance; class CGameState; struct CPath; class CGObjectInstance; class CArmedInstance; struct BattleAction; class CGTownInstance; struct lua_State; class CClient; class IShipyard; struct CGPathNode; struct CGPath; struct CPathsInfo; 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(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, int id1, int id2, int val1, const CGHeroInstance *hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce virtual int selectionMade(int selection, 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 CPack; class CBattleCallback : public IBattleCallback, public CPlayerBattleCallback { protected: int sendRequest(const CPack *request); //returns requestID (that'll be matched to requestID in PackageApplied) CClient *cl; //virtual bool hasAccess(int playerId) const; public: CBattleCallback(CGameState *GS, boost::optional Player, CClient *C); int battleMakeAction(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 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 int getMovementCost(const CGHeroInstance * hero, int3 dest); virtual int3 getGuardingCreaturePosition(int3 tile); virtual const CPathsInfo * 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(shared_ptr gameEvents); void registerBattleInterface(shared_ptr battleEvents); void unregisterGameInterface(shared_ptr gameEvents); void unregisterBattleInterface(shared_ptr battleEvents); void unregisterAllInterfaces(); //stops delivering information about game events to player interfaces -> can be called ONLY after victory/loss //commands bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false); //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); int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2); int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2); //first goes to the second int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2); //first goes to the second int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val); bool dismissHero(const CGHeroInstance * hero); //bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2); bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2); //bool moveArtifact(const CGHeroInstance * hero, ui16 src, const CStackInstance * stack, ui16 dest); // TODO: unify classes //bool moveArtifact(const CStackInstance * stack, ui16 src , const CGHeroInstance * hero, ui16 dest); // TODO: unify classes bool assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo); bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override; void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1); bool dismissCreature(const CArmedInstance *obj, SlotID stackPos); bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override; void endTurn(); void swapGarrisonHero(const CGTownInstance *town); void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; void trade(const CGObjectInstance *market, EMarketMode::EMarketMode mode, int id1, int id2, int val1, const CGHeroInstance *hero = nullptr); void setFormation(const CGHeroInstance * hero, bool tight); void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero); void save(const std::string &fname); void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr); void buildBoat(const IShipyard *obj); void dig(const CGObjectInstance *hero); void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1)); //friends friend class CClient; }; vcmi-0.98/CMakeLists.txt000066400000000000000000000300131250671757600151650ustar00rootroot00000000000000project(vcmi) cmake_minimum_required(VERSION 2.8.12) # TODO: # 1) Detection of Qt5 and compilation of launcher, unless explicitly disabled # where to look for cmake modules set(CMAKE_MODULE_PATH ${CMAKE_HOME_DIRECTORY}/cmake_modules) # enable Release mode but only if it was not set if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo) endif() # VCMI version set(VCMI_VERSION_MAJOR 0) set(VCMI_VERSION_MINOR 97) set(VCMI_VERSION_PATCH 0) option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF) option(ENABLE_EDITOR "Enable compilation of map editor" OFF) option(ENABLE_LAUNCHER "Enable compilation of launcher" ON) option(ENABLE_TEST "Enable compilation of unit tests" OFF) option(ENABLE_PCH "Enable compilation using precompiled headers" ON) option(ENABLE_SDL2 "Use SDL2 for compilation instead of SDL 1.2" ON) ############################################ # Building section # ############################################ if (APPLE) # Default location for thirdparty libs set(CMAKE_INCLUDE_PATH "../include" "${CMAKE_OSX_SYSROOT}/usr/include") set(CMAKE_LIBRARY_PATH "../lib") set(CMAKE_FRAMEWORK_PATH "../Frameworks") set(BOOST_ROOT "../") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_HOME_DIRECTORY}/bin") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_HOME_DIRECTORY}/bin") set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_HOME_DIRECTORY}/bin") set(CMAKE_XCODE_ATTRIBUTE_CONFIGURATION_BUILD_DIR "${CMAKE_HOME_DIRECTORY}/bin/$(CONFIGURATION)") set(CMAKE_XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../Frameworks @executable_path/") # Build with clang ang libc++ set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++11") set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++") # On OS X we use Sparkle framework for updates find_path(SPARKLE_INCLUDE_DIR Sparkle.h) find_library(SPARKLE_FRAMEWORK NAMES Sparkle) # Xcode 5.0 fix set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ftemplate-depth=256") # Link with iconv set(SYSTEM_LIBS ${SYSTEM_LIBS} libiconv.dylib) endif() if (WIN32) add_definitions(-DBOOST_THREAD_USE_LIB) add_definitions(-D_WIN32_WINNT=0x0501) set(SYSTEM_LIBS ${SYSTEM_LIBS} ole32 oleaut32 ws2_32 mswsock) #delete lib prefix for dlls (libvcmi -> vcmi) set(CMAKE_SHARED_LIBRARY_PREFIX "") if(MINGW) #MinGW: 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() #MinGW: copy runtime to VCMI location get_filename_component(MINGW_BIN_PATH ${CMAKE_CXX_COMPILER} PATH ) set(dep_files ${dep_files} "${MINGW_BIN_PATH}/libwinpthread-*.dll") set(dep_files ${dep_files} "${MINGW_BIN_PATH}/libgcc_s_*.dll") set(dep_files ${dep_files} "${MINGW_BIN_PATH}/libstdc++-*.dll") #MinGW: use O1 to prevent compiler crash in some cases set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O1") endif() if(MSVC) #MSVC: Fix problems with linking add_definitions(-DBOOST_ALL_NO_LIB) set(Boost_USE_STATIC_LIBS ON) #MSVC: Don't link with SDLMain set(SDL2_BUILDING_LIBRARY ON) #MSVC: 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") endif() endif() if(NOT WIN32) INCLUDE(CheckLibraryExists) #check if some platform-specific libraries are needed for linking CHECK_LIBRARY_EXISTS(rt shm_open "" HAVE_RT_LIB) if(HAVE_RT_LIB) set(SYSTEM_LIBS ${SYSTEM_LIBS} rt) endif() CHECK_LIBRARY_EXISTS(dl dlopen "" HAVE_DL_LIB) if(HAVE_DL_LIB) set(SYSTEM_LIBS ${SYSTEM_LIBS} dl) endif() endif() set(FFmpeg_FIND_COMPONENTS AVFORMAT SWSCALE) find_package(Boost 1.48.0 COMPONENTS 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() if (ENABLE_SDL2) find_package(SDL2 REQUIRED) find_package(SDL2_image REQUIRED) find_package(SDL2_mixer REQUIRED) find_package(SDL2_ttf REQUIRED) set(SDL_INCLUDE_DIR "${SDL2_INCLUDE_DIR}") set(SDLTTF_INCLUDE_DIR "${SDL2_TTF_INCLUDE_DIR}") set(SDLIMAGE_INCLUDE_DIR "${SDL2_IMAGE_INCLUDE_DIR}") set(SDLMIXER_INCLUDE_DIR "${SDL2_MIXER_INCLUDE_DIR}") set(SDL_LIBRARY "${SDL2_LIBRARY}") set(SDLTTF_LIBRARY "${SDL2_TTF_LIBRARY}") set(SDLIMAGE_LIBRARY "${SDL2_IMAGE_LIBRARY}") set(SDLMIXER_LIBRARY "${SDL2_MIXER_LIBRARY}") else() find_package(SDL REQUIRED) find_package(SDL_image REQUIRED) find_package(SDL_mixer REQUIRED) find_package(SDL_ttf REQUIRED) endif() include(cotire) if (ENABLE_EDITOR OR ENABLE_LAUNCHER) # Widgets finds its own dependencies (QtGui and QtCore). find_package(Qt5Widgets REQUIRED) endif() if (ENABLE_LAUNCHER) find_package(Qt5Network REQUIRED) endif() if(ENABLE_TEST) # find_package overwrites BOOST_* variables which are already set, so all components have to be included again find_package(Boost 1.48.0 COMPONENTS program_options filesystem system thread locale unit_test_framework REQUIRED) endif() if(CMAKE_COMPILER_IS_GNUCXX OR NOT WIN32) #so far all *nix compilers support such parameters if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(CLANG_SPECIFIC_FLAGS "-Wno-mismatched-tags -Wno-unknown-warning-option") endif() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wall -Wextra -Wpointer-arith -Wno-strict-aliasing -Wno-switch -Wno-sign-compare -Wno-unused-local-typedefs -Wno-unused-parameter -Wuninitialized -Wno-overloaded-virtual -Wno-type-limits -Wno-unknown-pragmas -Wno-reorder ${CLANG_SPECIFIC_FLAGS}") if(UNIX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden") endif() endif() 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) set(BIN_DIR "." CACHE STRING "Where to install binaries") set(LIB_DIR "." CACHE STRING "Where to install main library") set(DATA_DIR "../h3" CACHE STRING "Where to install data files") else() # includes lib path which determines where to install shared libraries (either /lib or /lib64) include(GNUInstallDirs) 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() set (AI_LIB_DIR "${LIB_DIR}/AI") set (SCRIPTING_LIB_DIR "${LIB_DIR}/scripting") #define required constants add_definitions(-DM_DATA_DIR="${CMAKE_INSTALL_PREFIX}/${DATA_DIR}") add_definitions(-DM_BIN_DIR="${CMAKE_INSTALL_PREFIX}/${BIN_DIR}") add_definitions(-DM_LIB_DIR="${CMAKE_INSTALL_PREFIX}/${LIB_DIR}") SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/vcmi") SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) # precompiled header configuration SET(PCH_PROPERTIES COTIRE_ENABLE_PRECOMPILED_HEADER ${ENABLE_PCH} COTIRE_ADD_UNITY_BUILD FALSE COTIRE_CXX_PREFIX_HEADER_INIT "StdInc.h" ) if (ENABLE_ERM) add_subdirectory(scripting/erm) endif() if (NOT MINIZIP_FOUND) add_subdirectory(lib/minizip) set(MINIZIP_LIBRARIES minizip) endif() add_subdirectory(lib) add_subdirectory(client) add_subdirectory(server) add_subdirectory(AI) if (ENABLE_EDITOR) add_subdirectory(editor) endif() if (ENABLE_LAUNCHER) add_subdirectory(launcher) endif() if(ENABLE_TEST) add_subdirectory(test) endif() ####################################### # Installation section # ####################################### # For apple this files will be already inside vcmiclient bundle if (NOT APPLE) # copy whole directory but .svn control files install(DIRECTORY config DESTINATION ${DATA_DIR} PATTERN ".svn" EXCLUDE) # copy vcmi mod along with all its content install(DIRECTORY Mods/vcmi DESTINATION ${DATA_DIR}/Mods PATTERN ".svn" EXCLUDE) install(FILES vcmibuilder DESTINATION ${BIN_DIR} PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) endif() if(WIN32) file(GLOB dep_files ${dep_files} "${CMAKE_FIND_ROOT_PATH}/bin/*.dll") #Copy debug versions of libraries if build type is debug. Doesn't work in MSVC! if(CMAKE_BUILD_TYPE MATCHES DEBUG) set(debug_postfix d) endif(CMAKE_BUILD_TYPE MATCHES DEBUG) if (ENABLE_EDITOR OR 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) if(MSVC) file(GLOB dep_files ${dep_files} ${Qtbin_folder}/libEGL.dll ${Qtbin_folder}/libGLESv2.dll) endif() endif() if (ENABLE_LAUNCHER) file(GLOB dep_files ${dep_files} ${Qtbin_folder}/Qt5Network${debug_postfix}.dll) endif() if(MSVC) #install MSVC runtime include(InstallRequiredSystemLibraries) install(FILES ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} DESTINATION ${BIN_DIR}) endif() install(FILES ${dep_files} DESTINATION ${BIN_DIR}) install(FILES ${dep_qwindows} DESTINATION ${BIN_DIR}/platforms) elseif(NOT APPLE) #install icons and desktop file on Linux #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) if (ENABLE_LAUNCHER) #FIXME: move to launcher makefile? install(FILES "${CMAKE_SOURCE_DIR}/launcher/vcmilauncher.desktop" DESTINATION share/applications) endif() endif() ####################################### # 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(WIN32) set(CPACK_MONOLITHIC_INSTALL 1) set(CPACK_PACKAGE_NAME "VCMI") set(CPACK_PACKAGE_VENDOR "VCMI team") set(CPACK_PACKAGE_FILE_NAME "vcmi-${CPACK_PACKAGE_VERSION}-win32") set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/license.txt") set(CPACK_PACKAGE_EXECUTABLES "VCMI_launcher;VCMI") set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}") set(CPACK_NSIS_PACKAGE_NAME "VCMI ${CPACK_PACKAGE_VERSION}") set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES") set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_launcher.exe\\\"") set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" ") configure_file("${CMAKE_SOURCE_DIR}/cmake_modules/CMakeCPackOptions.cmake.in" "${CMAKE_BINARY_DIR}/CMakeCPackOptions.cmake" @ONLY) set(CPACK_PROJECT_CONFIG_FILE "${CMAKE_BINARY_DIR}/CMakeCPackOptions.cmake") elseif(APPLE) set(CPACK_GENERATOR DragNDrop) set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_SOURCE_DIR}/osx/dmg_background.png") set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/osx/dmg_DS_Store") else() set(CPACK_GENERATOR TGZ) endif() INCLUDE(CPack) vcmi-0.98/ChangeLog000066400000000000000000001460301250671757600142060ustar00rootroot000000000000000.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.98/Global.h000066400000000000000000000500271250671757600140050ustar00rootroot00000000000000#pragma once /* * 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 * */ /* ---------------------------------------------------------------------------- */ /* 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 /* ---------------------------------------------------------------------------- */ /* 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. #endif #define _USE_MATH_DEFINES #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //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 #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 #ifndef M_PI # define M_PI 3.14159265358979323846 #endif /* ---------------------------------------------------------------------------- */ /* Usings */ /* ---------------------------------------------------------------------------- */ using std::shared_ptr; using std::unique_ptr; using std::make_shared; using namespace std::placeholders; namespace range = boost::range; /* ---------------------------------------------------------------------------- */ /* Typedefs */ /* ---------------------------------------------------------------------------- */ // Integral data types typedef boost::uint64_t ui64; //unsigned int 64 bits (8 bytes) typedef boost::uint32_t ui32; //unsigned int 32 bits (4 bytes) typedef boost::uint16_t ui16; //unsigned int 16 bits (2 bytes) typedef boost::uint8_t ui8; //unsigned int 8 bits (1 byte) typedef boost::int64_t si64; //signed int 64 bits (8 bytes) typedef boost::int32_t si32; //signed int 32 bits (4 bytes) typedef boost::int16_t si16; //signed int 16 bits (2 bytes) typedef boost::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"))) # 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)) #define ASSERT_IF_CALLED_WITH_PLAYER if(!player) {logGlobal->errorStream() << BOOST_CURRENT_FUNCTION; assert(0);} // 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 "lib/logging/CLogger.h" void inline handleException() { try { throw; } catch(const std::exception & ex) { logGlobal->errorStream() << ex.what(); } catch(const std::string & ex) { logGlobal->errorStream() << ex; } catch(...) { logGlobal->errorStream() << "Sorry, caught unknown exception type. No more info available."; } } template std::ostream & operator<<(std::ostream & out, const boost::optional & opt) { if(opt) return out << *opt; else return out << "empty"; } template std::ostream & operator<<(std::ostream & out, const std::vector & container) { out << "["; for(auto it = container.begin(); it != container.end(); ++it) { out << *it; if(std::prev(container.end()) != it) out << ", "; } return out << "]"; } 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); } //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; } using boost::math::round; } using vstd::operator-=; using vstd::make_unique; vcmi-0.98/Mods/000077500000000000000000000000001250671757600133325ustar00rootroot00000000000000vcmi-0.98/Mods/vcmi/000077500000000000000000000000001250671757600142705ustar00rootroot00000000000000vcmi-0.98/Mods/vcmi/Data/000077500000000000000000000000001250671757600151415ustar00rootroot00000000000000vcmi-0.98/Mods/vcmi/Data/StackQueueLarge.png000066400000000000000000000044071250671757600207010ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Data/questDialog.png000066400000000000000000011754331250671757600201460ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Data/s/000077500000000000000000000000001250671757600154035ustar00rootroot00000000000000vcmi-0.98/Mods/vcmi/Data/s/std.verm000066400000000000000000000014701250671757600170720ustar00rootroot00000000000000VERM ; 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.98/Mods/vcmi/Data/s/testy.erm000066400000000000000000000006051250671757600172610ustar00rootroot00000000000000ZVSE !?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.98/Mods/vcmi/Data/stackWindow/000077500000000000000000000000001250671757600174365ustar00rootroot00000000000000vcmi-0.98/Mods/vcmi/Data/stackWindow/bonus-effects.png000066400000000000000000000724211250671757600227150ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Data/stackWindow/button-panel.png000066400000000000000000000742761250671757600225740ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Data/stackWindow/commander-abilities.png000066400000000000000000000616071250671757600240660ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Data/stackWindow/icons.png000066400000000000000000000100531250671757600212560ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Data/stackWindow/info-panel-0.png000066400000000000000000002574651250671757600223540ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Data/stackWindow/info-panel-1.png000066400000000000000000002545631250671757600223510ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Data/stackWindow/info-panel-2.png000066400000000000000000002450011250671757600223350ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Data/stackWindow/spell-effects.png000066400000000000000000000503061250671757600227040ustar00rootroot00000000000000PNG  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 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.98/Mods/vcmi/Sprites/buttons/commanderPressed.png000066400000000000000000000054521250671757600234260ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/buttons/resolution.json000066400000000000000000000002261250671757600225150ustar00rootroot00000000000000{ "basepath" : "buttons/", "images" : [ { "frame" : 0, "file" : "resolutionNormal.png"}, { "frame" : 1, "file" : "resolutionPressed.png"} ] } vcmi-0.98/Mods/vcmi/Sprites/buttons/resolutionNormal.png000066400000000000000000000124561250671757600235110ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/itpa.json000066400000000000000000000001551250671757600175520ustar00rootroot00000000000000{ "images" : [ { "frame" : 38, "file" : "HPSRAND0.bmp"}, { "frame" : 39, "file" : "HPSRAND5.bmp"} ] } vcmi-0.98/Mods/vcmi/Sprites/stackWindow/000077500000000000000000000000001250671757600202165ustar00rootroot00000000000000vcmi-0.98/Mods/vcmi/Sprites/stackWindow/cancel-normal.png000066400000000000000000000024301250671757600234360ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/cancel-pressed.png000066400000000000000000000023741250671757600236220ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/cancelButton.json000066400000000000000000000002241250671757600235300ustar00rootroot00000000000000{ "basepath" : "stackWindow/", "images" : [ { "frame" : 0, "file" : "cancel-normal.png"}, { "frame" : 1, "file" : "cancel-pressed.png"} ] } vcmi-0.98/Mods/vcmi/Sprites/stackWindow/level-0.png000066400000000000000000000002661250671757600221740ustar00rootroot00000000000000PNG  IHDR*,*FgAMA abKGD oFFsA -@ pHYs X =tIME  gIDATX  Om EPIENDB`vcmi-0.98/Mods/vcmi/Sprites/stackWindow/level-1.png000066400000000000000000000023661250671757600222000ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/level-10.png000066400000000000000000000021121250671757600222450ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/level-2.png000066400000000000000000000025051250671757600221740ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/level-4.png000066400000000000000000000023661250671757600222030ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/level-7.png000066400000000000000000000023311250671757600221760ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/level-8.png000066400000000000000000000026301250671757600222010ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/level-9.png000066400000000000000000000026551250671757600222110ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/levels.json000066400000000000000000000010021250671757600223740ustar00rootroot00000000000000{ "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.98/Mods/vcmi/Sprites/stackWindow/switchModeIcons.json000066400000000000000000000001471250671757600242150ustar00rootroot00000000000000{ "images" : [ { "frame" : 0, "file" : "SECSK32:69"}, { "frame" : 1, "file" : "SECSK32:28"} ] } vcmi-0.98/Mods/vcmi/Sprites/stackWindow/upgrade-normal.png000066400000000000000000000022671250671757600236500ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/upgrade-pressed.png000066400000000000000000000021131250671757600240130ustar00rootroot00000000000000PNG  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.98/Mods/vcmi/Sprites/stackWindow/upgradeButton.json000066400000000000000000000002261250671757600237340ustar00rootroot00000000000000{ "basepath" : "stackWindow/", "images" : [ { "frame" : 0, "file" : "upgrade-normal.png"}, { "frame" : 1, "file" : "upgrade-pressed.png"} ] } vcmi-0.98/Mods/vcmi/mod.json000066400000000000000000000006611250671757600157450ustar00rootroot00000000000000{ "name" : "VCMI essential files", "description" : "Essential files required for VCMI to run correctly", "version" : "0.0", "author" : "VCMI Team", "contact" : "http://forum.vcmi.eu/index.php", "modType" : "Graphical", "filesystem": { "DATA/" : [ {"type" : "dir", "path" : "/Data"} ], "SPRITES/": [ {"type" : "dir", "path" : "/Sprites"} ], "MAPS/": [ {"type" : "dir", "path" : "/Maps"} ] } } vcmi-0.98/README.linux000066400000000000000000000067661250671757600144650ustar00rootroot00000000000000This readme covers VCMI compilation on Unix-like systems. To run the game you will need: 1) Heroes 3 data files (SoD or Complete editions); 2) VCMI data pack (http://download.vcmi.eu/core.zip) All of them can be installed manually or using vcmibuilder script For complete installation instructions see VCMI wiki: http://wiki.vcmi.eu/index.php?title=Installation_on_Linux#Preparing_data I. Prerequisites To compile, the following packages (and their development counterparts) are needed to build: * libstdc++ devel * CMake build system * SDL and SDL-devel * SDL_mixer and SDL_mixer-devel * SDL_image and SDL_image-devel * SDL_ttf and SDL_ttf-devel * zlib and zlib-devel * (optional) Qt 5, widget and network modules * the ffmpeg libraries (libavformat and libswscale). Their name could be libavformat-devel and libswscale-devel, or ffmpeg-libs-devel or similar names. * boost c++ libraries v1.48+ (www.boost.org): - program-options - filesystem - system - thread - locale On Debian-based systems (e.g. Ubuntu) run: sudo apt-get install cmake g++ libsdl1.2debian libsdl-image1.2-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev zlib1g-dev libavformat-dev libswscale-dev libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev qtbase5-dev On RPM-based distributions (e.g. Fedora) run: sudo yum install cmake gcc-c++ SDL2-devel SDL2_image-devel SDL2_ttf-devel SDL2_mixer-devel boost boost-devel boost-filesystem boost-system boost-thread boost-program-options boost-locale zlib-devel ffmpeg-devel ffmpeg-libs qt5-qtbase-devel II. Getting the sources VCMI is still in development. We recommend the following initial directory structure: trunk trunk/vcmi -> contains sources and is under git control trunk/build -> contains build output, makefiles, object files,... You can get latest sources with subversion: git clone https://github.com/vcmi/vcmi.git III. Compilation Run configure: mkdir build && cd build cmake ../vcmi Additional options that you may want to use: To enable debugging: -DCMAKE_BUILD_TYPE=Debug To change installation directory: -DCMAKE_INSTALL_PREFIX=$absolute_path_to_directory Notice: The ../vcmi/ is not a typo, it will place makefile scripts into the build dir as the build dir is your working dir when calling CMake. Then build vcmi: make -j2 (j2 = compile with 2 threads, you can specify any value) That will generate vcmiclient, vcmiserver, vcmilauncher as well as 3 .so libraries. III. Installing binaries To install VCMI you can use "make install" command however generation of distribution-specific packages is usually a better idea. In most cases this can be achieved using tool called "checkinstall" If you're compiling vcmi for development puposes, the easiest is to use cmake prefix and then make install: # mkdir .../trunk/install # cmake -DCMAKE_INSTALL_PREFIX=.../trunk/install ../vcmi # make && make install # .../trunk/install/bin/vcmiclient it's better to use links instead. Go to /BIN_PATH/, and type: ln -s .../trunk/build/client/vcmiclient ln -s .../trunk/build/server/vcmiserver ln -s .../trunk/build/launcher/vcmilauncher Go to /LIB_PATH/vcmi, and type: ln -s .../trunk/build/lib/libvcmi.so libvcmi.so Go to /LIB_PATH/vcmi/AI, and type: ln -s .../trunk/build/AI/VCAI/VCAI.so ln -s .../trunk/build/AI/StupidAI/StupidAI.so ln -s .../trunk/build/AI/BattleAI/BattleAI.so Go to /DATA_PATH/vcmi, and type: ln -s .../trunk/source/config ln -s .../trunk/source/Mods vcmi-0.98/README.md000066400000000000000000000015371250671757600137150ustar00rootroot00000000000000# VCMI Project VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities. To use VCMI you need to own original data files. ## Links * Homepage: http://vcmi.eu/ * Wiki: http://wiki.vcmi.eu/ * Forums: http://forum.vcmi.eu/ * Bugtracker: http://bugs.vcmi.eu/ ## Installation For installation of latest release see release announcement on http://vcmi.eu/ For building from source see project wiki at http://wiki.vcmi.eu/ ## Copyright and license VCMI Project source code is licensed under GPL version 2 or later. VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: [https://github.com/vcmi/vcmi-assets] Copyright (C) 2007-2015 VCMI Team (check AUTHORS file for the contributors list) vcmi-0.98/VCMI_VS10.sln000066400000000000000000000150321250671757600144560ustar00rootroot00000000000000 Microsoft Visual Studio Solution File, Format Version 11.00 # Visual Studio 2010 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_client", "client\VCMI_client.vcxproj", "{8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_lib", "lib\VCMI_lib.vcxproj", "{B952FFC5-3039-4DE1-9F08-90ACDA483D8F}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_server", "server\VCMI_server.vcxproj", "{8AF697C3-465E-4910-B31B-576A9ECDB309}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StupidAI", "AI\StupidAI\StupidAI.vcxproj", "{15DABC90-234A-4B6B-9EEB-777C4768B82B}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ERM", "scripting\erm\ERM.vcxproj", "{8F202F43-106D-4F63-AD9D-B1D43E803E8C}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCAI", "AI\VCAI\VCAI.vcxproj", "{276C3DB0-7A6B-4417-8E5C-322B08633AAC}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} {D15B34EC-A32C-4968-9B0B-66998B579364} = {D15B34EC-A32C-4968-9B0B-66998B579364} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FuzzyLite", "AI\FuzzyLite\FuzzyLite.vcxproj", "{D15B34EC-A32C-4968-9B0B-66998B579364}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BattleAI", "AI\BattleAI\BattleAI.vcxproj", "{C0300513-E845-43B4-9A4F-E8817EAEF57C}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 Debug|x64 = Debug|x64 RD|Win32 = RD|Win32 RD|x64 = RD|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|Win32.ActiveCfg = Debug|Win32 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|Win32.Build.0 = Debug|Win32 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|x64.ActiveCfg = Debug|x64 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|x64.Build.0 = Debug|x64 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|Win32.ActiveCfg = RD|Win32 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|Win32.Build.0 = RD|Win32 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|x64.ActiveCfg = RD|x64 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|x64.Build.0 = RD|x64 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|Win32.ActiveCfg = Debug|Win32 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|Win32.Build.0 = Debug|Win32 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|x64.ActiveCfg = Debug|x64 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|x64.Build.0 = Debug|x64 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|Win32.ActiveCfg = RD|Win32 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|Win32.Build.0 = RD|Win32 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|x64.ActiveCfg = RD|x64 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|x64.Build.0 = RD|x64 {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|Win32.ActiveCfg = Debug|Win32 {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|Win32.Build.0 = Debug|Win32 {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|x64.ActiveCfg = Debug|x64 {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|x64.Build.0 = Debug|x64 {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|Win32.ActiveCfg = RD|Win32 {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|Win32.Build.0 = RD|Win32 {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|x64.ActiveCfg = RD|x64 {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|x64.Build.0 = RD|x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|Win32.ActiveCfg = Debug|Win32 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|Win32.Build.0 = Debug|Win32 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|x64.ActiveCfg = Debug|x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|x64.Build.0 = Debug|x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|Win32.ActiveCfg = RD|Win32 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|Win32.Build.0 = RD|Win32 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|x64.ActiveCfg = RD|x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|x64.Build.0 = RD|x64 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|Win32.ActiveCfg = Debug|Win32 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|Win32.Build.0 = Debug|Win32 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|x64.ActiveCfg = Debug|x64 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|x64.Build.0 = Debug|x64 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|Win32.ActiveCfg = RD|Win32 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|Win32.Build.0 = RD|Win32 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|x64.ActiveCfg = RD|x64 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|x64.Build.0 = RD|x64 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|Win32.ActiveCfg = Debug|Win32 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|Win32.Build.0 = Debug|Win32 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|x64.ActiveCfg = Debug|Win32 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|Win32.ActiveCfg = RD|Win32 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|Win32.Build.0 = RD|Win32 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|x64.ActiveCfg = RD|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.Debug|Win32.ActiveCfg = Debug|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.Debug|Win32.Build.0 = Debug|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.Debug|x64.ActiveCfg = Debug|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.RD|Win32.ActiveCfg = RD|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.RD|Win32.Build.0 = RD|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.RD|x64.ActiveCfg = RD|Win32 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|Win32.ActiveCfg = Debug|Win32 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|Win32.Build.0 = Debug|Win32 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|x64.ActiveCfg = Debug|x64 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|x64.Build.0 = Debug|x64 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|Win32.ActiveCfg = RD|Win32 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|Win32.Build.0 = RD|Win32 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|x64.ActiveCfg = RD|x64 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|x64.Build.0 = RD|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal vcmi-0.98/VCMI_VS11.sln000066400000000000000000000244651250671757600144710ustar00rootroot00000000000000 Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2012 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_client", "client\VCMI_client.vcxproj", "{8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_lib", "lib\VCMI_lib.vcxproj", "{B952FFC5-3039-4DE1-9F08-90ACDA483D8F}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_server", "server\VCMI_server.vcxproj", "{8AF697C3-465E-4910-B31B-576A9ECDB309}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StupidAI", "AI\StupidAI\StupidAI.vcxproj", "{15DABC90-234A-4B6B-9EEB-777C4768B82B}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ERM", "scripting\erm\ERM.vcxproj", "{8F202F43-106D-4F63-AD9D-B1D43E803E8C}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCAI", "AI\VCAI\VCAI.vcxproj", "{276C3DB0-7A6B-4417-8E5C-322B08633AAC}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} {D15B34EC-A32C-4968-9B0B-66998B579364} = {D15B34EC-A32C-4968-9B0B-66998B579364} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FuzzyLite", "AI\FuzzyLite\FuzzyLite.vcxproj", "{D15B34EC-A32C-4968-9B0B-66998B579364}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BattleAI", "AI\BattleAI\BattleAI.vcxproj", "{C0300513-E845-43B4-9A4F-E8817EAEF57C}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Editor", "editor\Editor.vcxproj", "{B12702AD-ABFB-343A-A199-8E24837244A3}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EmptyAI", "AI\EmptyAI\EmptyAI.vcxproj", "{C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "test\Test.vcxproj", "{BA25F3F0-EB87-4164-AAB9-073C50A3557A}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "minizip", "lib\minizip\minizip.vcxproj", "{AA3CC588-9D08-4178-A1E8-C71561E99723}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_launcher", "launcher\VCMI_launcher.vcxproj", "{5B6946C8-A24F-4223-8415-5E16A238ACED}" ProjectSection(ProjectDependencies) = postProject {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 Debug|x64 = Debug|x64 RD|Win32 = RD|Win32 RD|x64 = RD|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|Win32.ActiveCfg = RD|Win32 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|Win32.Build.0 = RD|Win32 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|x64.ActiveCfg = Debug|x64 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|x64.Build.0 = Debug|x64 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|Win32.ActiveCfg = RD|Win32 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|Win32.Build.0 = RD|Win32 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|x64.ActiveCfg = RD|x64 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|x64.Build.0 = RD|x64 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|Win32.ActiveCfg = RD|Win32 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|Win32.Build.0 = RD|Win32 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|x64.ActiveCfg = Debug|x64 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|x64.Build.0 = Debug|x64 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|Win32.ActiveCfg = RD|Win32 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|Win32.Build.0 = RD|Win32 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|x64.ActiveCfg = RD|x64 {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|x64.Build.0 = RD|x64 {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|Win32.ActiveCfg = RD|Win32 {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|Win32.Build.0 = RD|Win32 {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|x64.ActiveCfg = Debug|x64 {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|x64.Build.0 = Debug|x64 {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|Win32.ActiveCfg = RD|Win32 {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|Win32.Build.0 = RD|Win32 {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|x64.ActiveCfg = RD|x64 {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|x64.Build.0 = RD|x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|Win32.ActiveCfg = RD|Win32 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|Win32.Build.0 = RD|Win32 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|x64.ActiveCfg = Debug|x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|x64.Build.0 = Debug|x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|Win32.ActiveCfg = RD|Win32 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|Win32.Build.0 = RD|Win32 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|x64.ActiveCfg = RD|x64 {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|x64.Build.0 = RD|x64 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|Win32.ActiveCfg = RD|Win32 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|Win32.Build.0 = RD|Win32 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|x64.ActiveCfg = Debug|x64 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|x64.Build.0 = Debug|x64 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|Win32.ActiveCfg = RD|Win32 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|Win32.Build.0 = RD|Win32 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|x64.ActiveCfg = RD|x64 {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|x64.Build.0 = RD|x64 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|Win32.ActiveCfg = RD|Win32 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|Win32.Build.0 = RD|Win32 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|x64.ActiveCfg = Debug|x64 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|x64.Build.0 = Debug|x64 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|Win32.ActiveCfg = RD|Win32 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|Win32.Build.0 = RD|Win32 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|x64.ActiveCfg = RD|x64 {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|x64.Build.0 = RD|x64 {D15B34EC-A32C-4968-9B0B-66998B579364}.Debug|Win32.ActiveCfg = RD|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.Debug|Win32.Build.0 = RD|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.Debug|x64.ActiveCfg = Debug|x64 {D15B34EC-A32C-4968-9B0B-66998B579364}.Debug|x64.Build.0 = Debug|x64 {D15B34EC-A32C-4968-9B0B-66998B579364}.RD|Win32.ActiveCfg = RD|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.RD|Win32.Build.0 = RD|Win32 {D15B34EC-A32C-4968-9B0B-66998B579364}.RD|x64.ActiveCfg = RD|x64 {D15B34EC-A32C-4968-9B0B-66998B579364}.RD|x64.Build.0 = RD|x64 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|Win32.ActiveCfg = RD|Win32 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|Win32.Build.0 = RD|Win32 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|x64.ActiveCfg = Debug|x64 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|x64.Build.0 = Debug|x64 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|Win32.ActiveCfg = RD|Win32 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|Win32.Build.0 = RD|Win32 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|x64.ActiveCfg = RD|x64 {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|x64.Build.0 = RD|x64 {B12702AD-ABFB-343A-A199-8E24837244A3}.Debug|Win32.ActiveCfg = Release|Win32 {B12702AD-ABFB-343A-A199-8E24837244A3}.Debug|x64.ActiveCfg = Debug|Win32 {B12702AD-ABFB-343A-A199-8E24837244A3}.RD|Win32.ActiveCfg = Release|Win32 {B12702AD-ABFB-343A-A199-8E24837244A3}.RD|x64.ActiveCfg = Release|Win32 {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.Debug|Win32.ActiveCfg = RD|Win32 {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.Debug|Win32.Build.0 = RD|Win32 {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.Debug|x64.ActiveCfg = Debug|x64 {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.Debug|x64.Build.0 = Debug|x64 {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.RD|Win32.ActiveCfg = RD|Win32 {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.RD|Win32.Build.0 = RD|Win32 {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.RD|x64.ActiveCfg = RD|x64 {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.RD|x64.Build.0 = RD|x64 {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.Debug|Win32.ActiveCfg = RD|Win32 {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.Debug|Win32.Build.0 = RD|Win32 {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.Debug|x64.ActiveCfg = Debug|x64 {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.Debug|x64.Build.0 = Debug|x64 {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.RD|Win32.ActiveCfg = RD|Win32 {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.RD|Win32.Build.0 = RD|Win32 {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.RD|x64.ActiveCfg = RD|x64 {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.RD|x64.Build.0 = RD|x64 {AA3CC588-9D08-4178-A1E8-C71561E99723}.Debug|Win32.ActiveCfg = RD|Win32 {AA3CC588-9D08-4178-A1E8-C71561E99723}.Debug|Win32.Build.0 = RD|Win32 {AA3CC588-9D08-4178-A1E8-C71561E99723}.Debug|x64.ActiveCfg = Debug|x64 {AA3CC588-9D08-4178-A1E8-C71561E99723}.Debug|x64.Build.0 = Debug|x64 {AA3CC588-9D08-4178-A1E8-C71561E99723}.RD|Win32.ActiveCfg = RD|Win32 {AA3CC588-9D08-4178-A1E8-C71561E99723}.RD|Win32.Build.0 = RD|Win32 {AA3CC588-9D08-4178-A1E8-C71561E99723}.RD|x64.ActiveCfg = RD|x64 {AA3CC588-9D08-4178-A1E8-C71561E99723}.RD|x64.Build.0 = RD|x64 {5B6946C8-A24F-4223-8415-5E16A238ACED}.Debug|Win32.ActiveCfg = RD|Win32 {5B6946C8-A24F-4223-8415-5E16A238ACED}.Debug|Win32.Build.0 = RD|Win32 {5B6946C8-A24F-4223-8415-5E16A238ACED}.Debug|x64.ActiveCfg = Debug|Win32 {5B6946C8-A24F-4223-8415-5E16A238ACED}.RD|Win32.ActiveCfg = RD|Win32 {5B6946C8-A24F-4223-8415-5E16A238ACED}.RD|Win32.Build.0 = RD|Win32 {5B6946C8-A24F-4223-8415-5E16A238ACED}.RD|x64.ActiveCfg = RD|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal vcmi-0.98/VCMI_global.props000066400000000000000000000015251250671757600155760ustar00rootroot00000000000000 <_PropertySheetDisplayName>VCMI_global $(SolutionDir)..\libs\$(PlatformShortName);$(VCMI_Out);$(LibraryPath) $(SolutionDir)..\include;$(IncludePath) $(VCMI_Out)\ Console 4275 vcmi-0.98/VCMI_global_debug.props000066400000000000000000000013171250671757600167430ustar00rootroot00000000000000 $(SolutionDir) MultiThreadedDebugDLL true $(VCMI_Out) true vcmi-0.98/VCMI_global_release.props000066400000000000000000000021521250671757600172730ustar00rootroot00000000000000 C:\Temp\RD Full true Speed true MultiThreadedDLL true $(VCMI_Out) vcmi-0.98/VCMI_global_user.props000066400000000000000000000007251250671757600166350ustar00rootroot00000000000000 C:\Qt\Qt5.1.0-32-no-opengl\5.1.0\msvc2012\ $(QTDIR) vcmi-0.98/client/000077500000000000000000000000001250671757600137065ustar00rootroot00000000000000vcmi-0.98/client/CBitmapHandler.cpp000066400000000000000000000107001250671757600172250ustar00rootroot00000000000000#include "StdInc.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/CFileInfo.h" #include "SDL.h" #include "SDL_image.h" #include "CBitmapHandler.h" #include "CDefHandler.h" #include "gui/SDL_Extensions.h" #include "../lib/vcmi_endian.h" /* * 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 * */ bool isPCX(const ui8 *header)//check whether file can be PCX according to header { int fSize = read_le_u32(header + 0); int width = read_le_u32(header + 4); int 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(SDL_SWSURFACE, 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++]; CSDL_Ext::colorSetAlpha(tp,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(SDL_SWSURFACE, 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->warnStream() << "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->errorStream()<<"Failed to open "<format->palette) { //set correct value for alpha\unused channel for (int i=0; i < ret->format->palette->ncolors; i++) CSDL_Ext::colorSetAlpha(ret->format->palette->colors[i],SDL_ALPHA_OPAQUE); } } else { logGlobal->errorStream()<<"Failed to open "<errorStream()<<"Reason: " << 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 // always set { CSDL_Ext::setDefaultColorKey(ret); } return ret; } SDL_Surface * BitmapHandler::loadBitmap(std::string fname, bool setKey) { SDL_Surface *bitmap; if (!(bitmap = loadBitmapFromDir("DATA/", fname, setKey)) && !(bitmap = loadBitmapFromDir("SPRITES/", fname, setKey))) logGlobal->errorStream()<<"Error: Failed to find file "<(table); ui8 *p; defName = name; DEFType = read_le_u32(&de.DEFType); width = read_le_u32(&de.width); height = read_le_u32(&de.height); ui32 totalBlocks = read_le_u32(&de.totalBlocks); for (ui32 it=0;it<256;it++) { palette[it].r = de.palette[it].R; palette[it].g = de.palette[it].G; palette[it].b = de.palette[it].B; CSDL_Ext::colorSetAlpha(palette[it],SDL_ALPHA_OPAQUE); } // The SDefEntryBlock starts just after the SDefEntry p = reinterpret_cast(&de); p += sizeof(de); int totalEntries=0; for (ui32 z=0; z(p); ui32 totalInBlock; totalInBlock = read_le_u32(&block.totalInBlock); for (ui32 j=SEntries.size(); j> 5; BR = N & 0x1F; } SDL_Surface * CDefHandler::getSprite (int SIndex, const ui8 * FDef, const SDL_Color * palette) const { SDL_Surface * ret=nullptr; ui32 BaseOffset, SpriteWidth, SpriteHeight, //format of sprite TotalRowLength, // length of read segment add, FullHeight,FullWidth, RowAdd, defType2; int LeftMargin, RightMargin, TopMargin, BottomMargin; ui8 SegmentType; BaseOffset = SEntries[SIndex].offset; SSpriteDef sd = * reinterpret_cast(FDef + BaseOffset); defType2 = read_le_u32(&sd.defType2); FullWidth = read_le_u32(&sd.FullWidth); FullHeight = read_le_u32(&sd.FullHeight); SpriteWidth = read_le_u32(&sd.SpriteWidth); SpriteHeight = read_le_u32(&sd.SpriteHeight); LeftMargin = read_le_u32(&sd.LeftMargin); TopMargin = read_le_u32(&sd.TopMargin); RightMargin = FullWidth - SpriteWidth - LeftMargin; BottomMargin = FullHeight - SpriteHeight - TopMargin; //if(LeftMargin + RightMargin < 0) // SpriteWidth += LeftMargin + RightMargin; //ugly construction... TODO: check how to do it nicer if(LeftMargin<0) SpriteWidth+=LeftMargin; if(RightMargin<0) SpriteWidth+=RightMargin; // Note: this looks bogus because we allocate only FullWidth, not FullWidth+add add = 4 - FullWidth%4; if (add==4) add=0; ret = SDL_CreateRGBSurface(SDL_SWSURFACE, FullWidth, FullHeight, 8, 0, 0, 0, 0); if(nullptr == ret) { logGlobal->errorStream() << __FUNCTION__ <<": Unable to create surface"; logGlobal->errorStream() << FullWidth << "X" << FullHeight; logGlobal->errorStream() << SDL_GetError(); throw std::runtime_error("Unable to create surface"); } BaseOffset += sizeof(SSpriteDef); int BaseOffsetor = BaseOffset; #ifdef VCMI_SDL1 for(int i=0; i<256; ++i) { SDL_Color pr; pr.r = palette[i].r; pr.g = palette[i].g; pr.b = palette[i].b; pr.unused = palette[i].unused; (*(ret->format->palette->colors+i))=pr; } #else if(SDL_SetPaletteColors(ret->format->palette,palette,0,256) != 0) { throw std::runtime_error("Unable to set palette"); } #endif int ftcp=0; // If there's a margin anywhere, just blank out the whole surface. if (TopMargin > 0 || BottomMargin > 0 || LeftMargin > 0 || RightMargin > 0) { memset( reinterpret_cast(ret->pixels), 0, FullHeight*FullWidth); } // Skip top margin if (TopMargin > 0) ftcp += TopMargin*(FullWidth+add); switch(defType2) { case 0: { for (ui32 i=0;i0) ftcp += LeftMargin; memcpy(reinterpret_cast(ret->pixels)+ftcp, &FDef[BaseOffset], SpriteWidth); ftcp += SpriteWidth; BaseOffset += SpriteWidth; if (RightMargin>0) ftcp += RightMargin; } } break; case 1: { const ui32 * RWEntriesLoc = reinterpret_cast(FDef+BaseOffset); BaseOffset += sizeof(int) * SpriteHeight; for (ui32 i=0;i0) ftcp += LeftMargin; TotalRowLength=0; do { ui32 SegmentLength; SegmentType=FDef[BaseOffset++]; SegmentLength=FDef[BaseOffset++] + 1; if (SegmentType==0xFF) { memcpy(reinterpret_cast(ret->pixels)+ftcp, FDef + BaseOffset, SegmentLength); BaseOffset+=SegmentLength; } else { memset(reinterpret_cast(ret->pixels)+ftcp, SegmentType, SegmentLength); } ftcp += SegmentLength; TotalRowLength += SegmentLength; }while(TotalRowLength0) ftcp += RightMargin; if (add>0) ftcp += add+RowAdd; } } break; case 2: { BaseOffset = BaseOffsetor + read_le_u16(FDef + BaseOffsetor); for (ui32 i=0;i0) ftcp += LeftMargin; TotalRowLength=0; do { SegmentType=FDef[BaseOffset++]; ui8 code = SegmentType / 32; ui8 value = (SegmentType & 31) + 1; if(code==7) { memcpy(reinterpret_cast(ret->pixels)+ftcp, &FDef[BaseOffset], value); ftcp += value; BaseOffset += value; } else { memset(reinterpret_cast(ret->pixels)+ftcp, code, value); ftcp += value; } TotalRowLength+=value; } while(TotalRowLength0) ftcp += RightMargin; RowAdd=SpriteWidth-TotalRowLength; if (add>0) ftcp += add+RowAdd; } } break; case 3: { for (ui32 i=0;i0) ftcp += LeftMargin; TotalRowLength=0; do { SegmentType=FDef[BaseOffset++]; ui8 code = SegmentType / 32; ui8 value = (SegmentType & 31) + 1; int len = std::min(value, SpriteWidth - TotalRowLength) - std::max(0, -LeftMargin); vstd::amax(len, 0); if(code==7) { memcpy((ui8*)ret->pixels + ftcp, FDef + BaseOffset, len); ftcp += len; BaseOffset += len; } else { memset((ui8*)ret->pixels + ftcp, code, len); ftcp += len; } TotalRowLength+=( LeftMargin>=0 ? value : value+LeftMargin ); }while(TotalRowLength0) ftcp += RightMargin; RowAdd=SpriteWidth-TotalRowLength; if (add>0) ftcp += add+RowAdd; } } break; default: throw std::runtime_error("Unknown sprite format."); break; } SDL_Color ttcol = ret->format->palette->colors[0]; #ifdef VCMI_SDL1 Uint32 keycol = SDL_MapRGBA(ret->format, ttcol.r, ttcol.b, ttcol.g, ttcol.unused); SDL_SetColorKey(ret, SDL_SRCCOLORKEY, keycol); #else Uint32 keycol = SDL_MapRGBA(ret->format, ttcol.r, ttcol.b, ttcol.g, ttcol.a); SDL_SetColorKey(ret, SDL_TRUE, keycol); #endif // 0 return ret; } CDefEssential * CDefHandler::essentialize() { auto ret = new CDefEssential; ret->ourImages = ourImages; notFreeImgs = true; return ret; } CDefHandler * CDefHandler::giveDef(const std::string & defName) { ResourceID resID(std::string("SPRITES/") + defName, EResType::ANIMATION); auto data = CResourceHandler::get()->load(resID)->readAll().first; if(!data) throw std::runtime_error("bad def name!"); auto nh = new CDefHandler(); nh->openFromMemory(data.get(), defName); return nh; } CDefEssential * CDefHandler::giveDefEss(const std::string & defName) { CDefEssential * ret; CDefHandler * temp = giveDef(defName); ret = temp->essentialize(); delete temp; return ret; } vcmi-0.98/client/CDefHandler.h000066400000000000000000000043321250671757600161600ustar00rootroot00000000000000#pragma once #include "../lib/vcmi_endian.h" struct SDL_Surface; struct SDL_Color; /* * CDefHandler.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ struct Cimage { int groupNumber; std::string imName; //name without extension SDL_Surface * bitmap; }; // Def entry in file. Integer fields are all little endian and will // need to be converted. struct SDefEntryBlock { ui32 unknown1; ui32 totalInBlock; ui32 unknown2; ui32 unknown3; ui8 data[0]; } PACKED_STRUCT; // Def entry in file. Integer fields are all little endian and will // need to be converted. struct SDefEntry { ui32 DEFType; ui32 width; ui32 height; ui32 totalBlocks; struct { ui8 R; ui8 G; ui8 B; } palette[256]; // SDefEntry is followed by a series of SDefEntryBlock // This is commented out because VC++ doesn't accept C99 syntax. //struct SDefEntryBlock blocks[]; } PACKED_STRUCT; // Def entry in file. Integer fields are all little endian and will // need to be converted. struct SSpriteDef { ui32 prSize; ui32 defType2; ui32 FullWidth; ui32 FullHeight; ui32 SpriteWidth; ui32 SpriteHeight; ui32 LeftMargin; ui32 TopMargin; } PACKED_STRUCT; class CDefEssential //DefHandler with images only { public: std::vector ourImages; ~CDefEssential(); //d-tor }; class CDefHandler { private: ui32 DEFType; struct SEntry { std::string name; int offset; int group; } ; std::vector SEntries ; public: int width, height; //width and height std::string defName; std::vector ourImages; bool notFreeImgs; CDefHandler(); //c-tor ~CDefHandler(); //d-tor SDL_Surface * getSprite (int SIndex, const ui8 * FDef, const SDL_Color * palette) const; //saves picture with given number to "testtt.bmp" static void expand(ui8 N,ui8 & BL, ui8 & BR); void openFromMemory(ui8 * table, const std::string & name); CDefEssential * essentialize(); static CDefHandler * giveDef(const std::string & defName); static CDefEssential * giveDefEss(const std::string & defName); }; vcmi-0.98/client/CGameInfo.cpp000066400000000000000000000012721250671757600162040ustar00rootroot00000000000000#include "StdInc.h" #include "CGameInfo.h" #include "../lib/VCMI_Lib.h" /* * 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 * */ const CGameInfo * CGI; //game info for general use CClientState * CCS = nullptr; CGameInfo::CGameInfo() { mh = 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; objtypeh = VLC->objtypeh; } vcmi-0.98/client/CGameInfo.h000066400000000000000000000030501250671757600156450ustar00rootroot00000000000000#pragma once #include "../lib/ConstTransitivePtr.h" /* * 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 * */ class CModHandler; class CMapHandler; class CArtHandler; class CHeroHandler; class CCreatureHandler; class CSpellHandler; class CBuildingHandler; class CObjectHandler; class CSoundHandler; class CMusicHandler; class CObjectClassesHandler; class CTownHandler; class CGeneralTextHandler; class CConsoleHandler; class CCursorHandler; class CGameState; class IMainVideoPlayer; 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 objh; ConstTransitivePtr objtypeh; CGeneralTextHandler * generaltexth; CMapHandler * mh; CTownHandler * townh; void setFromLib(); friend class CClient; CGameInfo(); }; extern const CGameInfo* CGI; vcmi-0.98/client/CMT.cpp000066400000000000000000001077261250671757600150520ustar00rootroot00000000000000// CMT.cpp : Defines the entry point for the console application. // #include "StdInc.h" #include #include "gui/SDL_Extensions.h" #include "CGameInfo.h" #include "mapHandler.h" #include "../lib/filesystem/Filesystem.h" #include "CPreGame.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 "CVideoHandler.h" #include "CDefHandler.h" #include "../lib/CGeneralTextHandler.h" #include "Graphics.h" #include "Client.h" #include "../lib/CConfigHandler.h" #include "../lib/Connection.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/CondSh.h" #ifdef VCMI_WINDOWS #include "SDL_syswm.h" #endif #include "../lib/UnlockGuard.h" #include "CMT.h" #if __MINGW32__ #undef main #endif namespace po = boost::program_options; namespace bfs = boost::filesystem; /* * 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 * */ std::string NAME_AFFIX = "client"; std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name CGuiHandler GH; static CClient *client=nullptr; #ifndef VCMI_SDL1 int preferredDriverIndex = -1; SDL_Window * mainWindow = nullptr; SDL_Renderer * mainRenderer = nullptr; SDL_Texture * screenTexture = nullptr; #endif // VCMI_SDL1 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; bool gNoGUI = false; 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, bool resetVideo=true); void dispose(); void playIntro(); static void mainLoop(); //void requestChangingResolution(); void startGame(StartInfo * options, CConnection *serv = nullptr); void endGame(); #ifndef VCMI_WINDOWS #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #endif void startGameFromFile(const bfs::path &fname) { StartInfo si; try //attempt retrieving start info from given file { if(fname.empty() || !bfs::exists(fname)) throw std::runtime_error("Startfile \"" + fname.string() + "\" does not exist!"); CLoadFile out(fname); if (!out.sfile || !*out.sfile) throw std::runtime_error("Cannot read from startfile \"" + fname.string() +"\"!"); out >> si; } catch(std::exception &e) { logGlobal->errorStream() << "Failed to start from the file: " << fname << ". Error: " << e.what() << " Falling back to main menu."; GH.curInt = CGPreGame::create(); return; } while(GH.topInt()) GH.popIntTotally(GH.topInt()); startGame(&si); } void init() { CStopWatch tmh, pomtime; loadDLLClasses(); const_cast(CGI)->setFromLib(); logGlobal->infoStream()<<"Initializing VCMI_Lib: "<curh = new CCursorHandler; graphics = new Graphics(); // should be before curh->init() CCS->curh->initCursor(); CCS->curh->show(); logGlobal->infoStream()<<"Screen handler: "<loadHeroAnims(); logGlobal->infoStream()<<"\tMain graphics: "<infoStream()<<"Initializing game graphics: "<infoStream()<<"Message handler: "<(), "runs game in duel mode (battle-only") ("start", po::value(), "starts game from saved StartInfo file") ("onlyAI", "runs without human player, all players will be default AI") ("noGUI", "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") ("loadserver","specifies we are the multiplayer server for loaded games") ("loadnumplayers",po::value(),"specifies the number of players connecting to a multiplayer game") ("loadhumanplayerindices",po::value>(),"Indexes of human players (0=Red, etc.)") ("loadplayer", po::value(),"specifies which player we are in multiplayer loaded games (0=Red, etc.)") ("loadserverip",po::value(),"IP for loaded game server") ("loadserverport",po::value(),"port for loaded game server"); if(argc > 1) { try { po::store(po::parse_command_line(argc, argv, opts), 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; } if(vm.count("noGUI")) { gNoGUI = true; vm.insert(std::pair("onlyAI", po::variable_value())); } #ifdef VCMI_SDL1 //Set environment vars to make window centered. Sometimes work, sometimes not. :/ putenv((char*)"SDL_VIDEO_WINDOW_POS"); putenv((char*)"SDL_VIDEO_CENTERED=1"); #endif // Have effect on X11 system only (Linux). // For whatever reason in fullscreen mode SDL takes "raw" mouse input from DGA X11 extension // (DGA = Direct graphics access). Because this is raw input (before any speed\acceleration proceesing) // it may result in very small \ very fast mouse when game in fullscreen mode putenv((char*)"SDL_VIDEO_X11_DGAMOUSE=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(); atexit(dispose); const bfs::path logPath = VCMIDirs::get().userCachePath() / "VCMI_Client_log.txt"; CBasicLogConfigurator logConfig(logPath, console); logConfig.configureDefault(); logGlobal->infoStream() << "Creating console and configuring logger: " << pomtime.getDiff(); logGlobal->infoStream() << "The log file will be saved to " << logPath; // Init filesystem and settings preinitDLL(::console); settings.init(); // Initialize logging based on settings logConfig.configure(); // 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->errorStream() << "Error: " << message << " was not found!"; 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->infoStream() <<"Loading settings: "<infoStream() << NAME; 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->errorStream() << "Fatal error: failed to load settings!"; logGlobal->errorStream() << "Possible reasons:"; logGlobal->errorStream() << "\tCorrupted local configuration file at " << VCMIDirs::get().userConfigPath() << "/settings.json"; logGlobal->errorStream() << "\tMissing or corrupted global configuration file at " << VCMIDirs::get().userConfigPath() << "/schemas/settings.json"; logGlobal->errorStream() << "VCMI will now exit..."; exit(EXIT_FAILURE); } if(!gNoGUI) { #ifdef VCMI_SDL1 if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_AUDIO)) #else if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_AUDIO|SDL_INIT_NOPARACHUTE)) #endif { logGlobal->errorStream()<<"Something was wrong: "<< SDL_GetError(); exit(-1); } GH.mainFPSmng->init(); //(!)init here AFTER SDL_Init() while using SDL for FPS management atexit(SDL_Quit); #ifndef VCMI_SDL1 int driversCount = SDL_GetNumRenderDrivers(); std::string preferredDriverName = video["driver"].String(); logGlobal->infoStream() << "Found " << driversCount << " render drivers"; 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->infoStream() << "\t" << driverName << " (active)"; } else logGlobal->infoStream() << "\t" << driverName; } #endif // VCMI_SDL1 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->errorStream() << "Selected resolution " << resPair.first << "x" << resPair.second << " was not found!"; if (conf.guiOptions.empty()) { logGlobal->errorStream() << "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->errorStream() << "Falling back to " << newRes["width"].Float() << "x" << newRes["height"].Float(); } } setScreenRes(res["width"].Float(), res["height"].Float(), video["bitsPerPixel"].Float(), video["fullscreen"].Bool()); logGlobal->infoStream() <<"\tInitializing screen: "<videoh = new CEmptyVideoPlayer; #else if (!gNoGUI && !vm.count("disable-video")) CCS->videoh = new CVideoPlayer; else CCS->videoh = new CEmptyVideoPlayer; #endif logGlobal->infoStream()<<"\tInitializing video: "<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->infoStream()<<"Initializing screen and sound handling: "<infoStream()<<"Initialization of VCMI (together): "<(); if(!fileToStartFrom.empty() && bfs::exists(fileToStartFrom)) startGameFromFile(fileToStartFrom); //ommit pregame and start the game using settings from file else { if(!fileToStartFrom.empty()) { logGlobal->warnStream() << "Warning: cannot find given file to start from (" << fileToStartFrom << "). Falling back to main menu."; } GH.curInt = CGPreGame::create(); //will set CGP pointer to itself } } else { auto si = new StartInfo(); si->mode = StartInfo::DUEL; si->mapname = vm["battle"].as(); si->playerInfos[PlayerColor(0)].color = PlayerColor(0); si->playerInfos[PlayerColor(1)].color = PlayerColor(1); startGame(si); } if(!gNoGUI) { 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->infoStream() << sbuffer.str(); for(const CIntObject *child : obj->children) printInfoAboutIntObject(child, level+1); } void processCommand(const std::string &message) { std::istringstream readed; readed.str(message); std::string cn; //command name readed >> cn; if(LOCPLINT && LOCPLINT->cingconsole) LOCPLINT->cingconsole->print(message); if(ermInteractiveMode) { if(cn == "exit") { ermInteractiveMode = false; return; } else { if(client && client->erm) 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->errorStream() << "screen"; else if(screenBuf == screen2) logGlobal->errorStream() << "screen2"; else logGlobal->errorStream() << "?!?"; SDL_SaveBMP(screen, "Screen_c.bmp"); SDL_SaveBMP(screen2, "Screen2_c.bmp"); } else if(cn=="save") { std::string fname; readed >> fname; 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; client->loadGame(fname); } 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 == "onlyai") { vm.insert(std::pair("onlyAI", po::variable_value())); } else if (cn == "ai") { VLC->IS_AI_ENABLED = !VLC->IS_AI_ENABLED; std::cout << "Current AI status: " << (VLC->IS_AI_ENABLED ? "enabled" : "disabled") << std::endl; } 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") { std::cout << "Bonuses of " << adventureInt->selection->getObjectName() << std::endl << 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 << parent->getBonusList() << std::endl; } } else if(cn == "not dialog") { LOCPLINT->showingDialog->setn(false); } else if(cn == "gui") { for(const IShowActivatable *child : GH.listInt) { if(const CIntObject *obj = dynamic_cast(child)) printInfoAboutIntObject(obj, 0); else std::cout << typeid(*obj).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; else if (value == "off") conf->Bool() = false; } else if(cn == "sinfo") { std::string fname; readed >> fname; if(fname.size() && SEL) { CSaveFile out(fname); out << SEL->sInfo; } } else if(cn == "start") { std::string fname; readed >> fname; startGameFromFile(fname); } 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; if (CResourceHandler::get()->existsResource(ResourceID("SPRITES/" + URI))) { CDefEssential * cde = CDefHandler::giveDefEss(URI); const bfs::path outPath = VCMIDirs::get().userCachePath() / "extracted" / URI; bfs::create_directories(outPath); for (size_t i = 0; i < cde->ourImages.size(); ++i) { const bfs::path filePath = outPath / (boost::lexical_cast(i)+".bmp"); SDL_SaveBMP(cde->ourImages[i].bitmap, filePath.string().c_str()); } } else logGlobal->errorStream() << "File not found!"; } 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->errorStream() << "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->warnStream() << "Failed opening " << fname << ": " << e.what(); logGlobal->warnStream() << "Setting not changes, AI not found or invalid!"; } } else if(cn == "autoskip") { Settings session = settings.write["session"]; session["autoSkip"].Bool() = !session["autoSkip"].Bool(); } else if(client && client->serv && client->serv->connected && LOCPLINT) //send to server { boost::unique_lock un(*LOCPLINT->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", 60, 40, screen, true, true)) { CCS->videoh->openAndPlayVideo("AZVS.SMK", 60, 80, screen, true, true); } } void dispose() { if (console) delete console; // cleanup, mostly to remove false leaks from analyzer CResourceHandler::clear(); if (CCS) { CCS->musich->release(); CCS->soundh->release(); } CMessage::dispose(); } static bool checkVideoMode(int monitorIndex, int w, int h, int& bpp, bool fullscreen) { #ifndef VCMI_SDL1 SDL_DisplayMode mode; const int modeCount = SDL_GetNumDisplayModes(monitorIndex); for (int i = 0; i < modeCount; i++) { SDL_GetDisplayMode(0, i, &mode); if (!mode.w || !mode.h || (w >= mode.w && h >= mode.h)) { return true; } } return false; #else bpp = SDL_VideoModeOK(w, h, bpp, SDL_SWSURFACE|(fullscreen?SDL_FULLSCREEN:0)); return !(bpp==0); #endif // VCMI_SDL1 } #ifndef VCMI_SDL1 static bool recreateWindow(int w, int h, int bpp, bool fullscreen) { // VCMI will only work with 2 or 4 bytes per pixel vstd::amax(bpp, 16); vstd::amin(bpp, 32); if(bpp>16) bpp = 32; int suggestedBpp = bpp; if(!checkVideoMode(0,w,h,suggestedBpp,fullscreen)) { logGlobal->errorStream() << "Error: SDL says that " << w << "x" << h << " resolution is not available!"; return false; } bool bufOnScreen = (screenBuf == screen); 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; } if(nullptr != mainRenderer) { SDL_DestroyRenderer(mainRenderer); mainRenderer = nullptr; } if(nullptr != mainWindow) { SDL_DestroyWindow(mainWindow); mainWindow = nullptr; } if(fullscreen) { //in full-screen mode always use desktop resolution mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, 0, 0, SDL_WINDOW_FULLSCREEN_DESKTOP); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); } else { mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED, w, h, 0); } 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->infoStream() << "Created renderer " << info.name; SDL_RenderSetLogicalSize(mainRenderer, w, h); SDL_RenderSetViewport(mainRenderer, nullptr); #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->errorStream() << "Unable to create surface"; logGlobal->errorStream() << w << " "<< h << " "<< bpp; logGlobal->errorStream() << 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->errorStream() << "Unable to create screen texture"; logGlobal->errorStream() << 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; } #endif //used only once during initialization static void setScreenRes(int w, int h, int bpp, bool fullscreen, bool resetVideo) { #ifdef VCMI_SDL1 // VCMI will only work with 2, 3 or 4 bytes per pixel vstd::amax(bpp, 16); vstd::amin(bpp, 32); // Try to use the best screen depth for the display int suggestedBpp = SDL_VideoModeOK(w, h, bpp, SDL_SWSURFACE|(fullscreen?SDL_FULLSCREEN:0)); if(suggestedBpp == 0) { logGlobal->errorStream() << "Error: SDL says that " << w << "x" << h << " resolution is not available!"; return; } bool bufOnScreen = (screenBuf == screen); if(suggestedBpp != bpp) { logGlobal->infoStream() << boost::format("Using %s bpp (bits per pixel) for the video mode. Default or overridden setting was %s bpp.") % suggestedBpp % bpp; } //For some reason changing fullscreen via config window checkbox result in SDL_Quit event if (resetVideo) { if(screen) //screen has been already initialized SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_InitSubSystem(SDL_INIT_VIDEO); } if((screen = SDL_SetVideoMode(w, h, suggestedBpp, SDL_SWSURFACE|(fullscreen?SDL_FULLSCREEN:0))) == nullptr) { logGlobal->errorStream() << "Requested screen resolution is not available (" << w << "x" << h << "x" << suggestedBpp << "bpp)"; throw std::runtime_error("Requested screen resolution is not available\n"); } logGlobal->infoStream() << "New screen flags: " << screen->flags; if(screen2) SDL_FreeSurface(screen2); screen2 = CSDL_Ext::copySurface(screen); SDL_EnableUNICODE(1); SDL_WM_SetCaption(NAME.c_str(),""); //set window title SDL_ShowCursor(SDL_DISABLE); SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL); #ifdef VCMI_WINDOWS SDL_SysWMinfo wm; SDL_VERSION(&wm.version); int getwm = SDL_GetWMInfo(&wm); if(getwm == 1) { int sw = GetSystemMetrics(SM_CXSCREEN), sh = GetSystemMetrics(SM_CYSCREEN); RECT curpos; GetWindowRect(wm.window,&curpos); int ourw = curpos.right - curpos.left, ourh = curpos.bottom - curpos.top; SetWindowPos(wm.window, 0, (sw - ourw)/2, (sh - ourh)/2, 0, 0, SWP_NOZORDER|SWP_NOSIZE); } else { logGlobal->warnStream() << "Something went wrong, getwm=" << getwm; logGlobal->warnStream() << "SDL says: " << SDL_GetError(); logGlobal->warnStream() << "Window won't be centered."; } #endif //TODO: centering game window on other platforms (or does the environment do their job correctly there?) screenBuf = bufOnScreen ? screen : screen2; //setResolution = true; #else if(!recreateWindow(w,h,bpp,fullscreen)) { throw std::runtime_error("Requested screen resolution is not available\n"); } #endif // VCMI_SDL1 } static void fullScreenChanged() { boost::unique_lock lock(*LOCPLINT->pim); Settings full = settings.write["video"]["fullscreen"]; const bool toFullscreen = full->Bool(); auto bitsPerPixel = screen->format->BitsPerPixel; #ifdef VCMI_SDL1 bitsPerPixel = SDL_VideoModeOK(screen->w, screen->h, bitsPerPixel, SDL_SWSURFACE|(toFullscreen?SDL_FULLSCREEN:0)); if(bitsPerPixel == 0) { logGlobal->errorStream() << "Error: SDL says that " << screen->w << "x" << screen->h << " resolution is not available!"; return; } bool bufOnScreen = (screenBuf == screen); screen = SDL_SetVideoMode(screen->w, screen->h, bitsPerPixel, SDL_SWSURFACE|(toFullscreen?SDL_FULLSCREEN:0)); screenBuf = bufOnScreen ? screen : screen2; #else auto w = screen->w; auto h = screen->h; if(!recreateWindow(w,h,bitsPerPixel,toFullscreen)) { //will return false and report error if video mode is not supported return; } #endif 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))) { handleQuit(); return; } #ifdef VCMI_SDL1 //FIXME: this should work even in pregame else if(LOCPLINT && ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4) #else else if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4) #endif // VCMI_SDL1 { Settings full = settings.write["video"]["fullscreen"]; full->Bool() = !full->Bool(); return; } else if(ev.type == SDL_USEREVENT) { switch(ev.user.code) { case RETURN_TO_MAIN_MENU: { endGame(); GH.curInt = CGPreGame::create();; GH.defActionsDef = 63; } break; case STOP_CLIENT: client->endGame(false); break; case RESTART_GAME: { StartInfo si = *client->getStartInfo(true); endGame(); startGame(&si); } break; case PREPARE_RESTART_CAMPAIGN: { auto si = reinterpret_cast(ev.user.data1); endGame(); startGame(si); } break; case RETURN_TO_MENU_LOAD: endGame(); CGPreGame::create(); GH.defActionsDef = 63; CGP->update(); CGP->menu->switchToTab(vstd::find_pos(CGP->menu->menuNameToEntry, "load")); GH.curInt = CGP; break; case FULLSCREEN_TOGGLED: fullScreenChanged(); break; default: logGlobal->errorStream() << "Unknown user event. Code " << ev.user.code; break; } return; } { 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, 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); } GH.renderFrame(); } } void startGame(StartInfo * options, CConnection *serv/* = nullptr*/) { if(vm.count("onlyAI")) { auto ais = vm.count("ai") ? vm["ai"].as>() : std::vector(); int i = 0; for(auto & elem : options->playerInfos) { elem.second.playerID = PlayerSettings::PLAYER_AI; if(i < ais.size()) elem.second.name = ais[i++]; } } client = new CClient; CPlayerInterface::howManyPeople = 0; switch(options->mode) //new game { case StartInfo::NEW_GAME: case StartInfo::CAMPAIGN: case StartInfo::DUEL: client->newGame(serv, options); break; case StartInfo::LOAD_GAME: std::string fname = options->mapname; boost::algorithm::erase_last(fname,".vlgm1"); if(!vm.count("loadplayer")) client->loadGame(fname); else client->loadGame(fname,vm.count("loadserver"),vm.count("loadhumanplayerindices") ? vm["loadhumanplayerindices"].as>() : std::vector(),vm.count("loadnumplayers") ? vm["loadnumplayers"].as() : 1,vm["loadplayer"].as(),vm.count("loadserverip") ? vm["loadserverip"].as() : "", vm.count("loadserverport") ? vm["loadserverport"].as() : "3030"); break; } client->connectionHandler = new boost::thread(&CClient::run, client); } void endGame() { client->endGame(); vstd::clear_pointer(client); } void handleQuit() { auto quitApplication = []() { if(client) endGame(); delete console; console = nullptr; boost::this_thread::sleep(boost::posix_time::milliseconds(750)); if(!gNoGUI) SDL_Quit(); std::cout << "Ending...\n"; exit(0); }; if(client && LOCPLINT) { CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, 0); } else { quitApplication(); } } vcmi-0.98/client/CMT.h000066400000000000000000000011631250671757600145030ustar00rootroot00000000000000#pragma once #ifndef VCMI_SDL1 #include extern SDL_Texture * screenTexture; extern SDL_Window * mainWindow; extern SDL_Renderer * mainRenderer; #endif // VCMI_SDL2 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 extern bool gNoGUI; //if true there is no client window and game is silently played between AIs void handleQuit(); vcmi-0.98/client/CMakeLists.txt000066400000000000000000000113041250671757600164450ustar00rootroot00000000000000project(vcmiclient) cmake_minimum_required(VERSION 2.6) include_directories(${CMAKE_HOME_DIRECTORY} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/lib) include_directories(${SDL_INCLUDE_DIR} ${SDLIMAGE_INCLUDE_DIR} ${SDLMIXER_INCLUDE_DIR} ${SDLTTF_INCLUDE_DIR}) include_directories(${Boost_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIR} ${FFMPEG_INCLUDE_DIRS}) set(client_SRCS StdInc.cpp ../CCallback.cpp battle/CBattleInterface.cpp battle/CBattleAnimations.cpp battle/CBattleInterfaceClasses.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/CHeroWindow.cpp windows/CKingdomInterface.cpp windows/CQuestLog.cpp windows/CSpellWindow.cpp windows/CTradeWindow.cpp windows/CWindowObject windows/InfoWindows.cpp windows/GUIClasses.cpp CBitmapHandler.cpp CDefHandler.cpp CGameInfo.cpp Client.cpp CMessage.cpp CMT.cpp CMusicHandler.cpp CPlayerInterface.cpp CPreGame.cpp CVideoHandler.cpp Graphics.cpp mapHandler.cpp NetPacksClient.cpp ) set(client_HEADERS gui/SDL_Pixels.h gui/SDL_Compat.h ) if(APPLE) # OS X specific includes include_directories(${SPARKLE_INCLUDE_DIR}) # OS X specific source files set(client_SRCS ${client_SRCS} SDLMain.m OSX.mm Info.plist vcmi.icns ../osx/vcmi_dsa_public.pem) add_executable(vcmiclient MACOSX_BUNDLE ${client_SRCS} ${client_HEADERS}) # OS X specific libraries target_link_libraries(vcmiclient ${SPARKLE_FRAMEWORK}) # Because server and AI libs would be copies to bundle they need to be built before client add_dependencies(vcmiclient vcmiserver VCAI EmptyAI StupidAI BattleAI minizip) # Custom Info.plist set_target_properties(vcmiclient PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist) # Copy icon file and public key for Sparkle set_source_files_properties(vcmi.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_source_files_properties(../osx/vcmi_dsa_public.pem PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_target_properties(vcmiclient PROPERTIES XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../Frameworks @executable_path/") # Copy server executable, libs and game data to bundle set(BUNDLE_PATH ${CMAKE_HOME_DIRECTORY}/bin/$(CONFIGURATION)/vcmiclient.app/Contents) set(MakeVCMIBundle # Copy all needed binaries mkdir -p ${BUNDLE_PATH}/MacOS/AI && cp ${CMAKE_HOME_DIRECTORY}/bin/$(CONFIGURATION)/vcmiserver ${BUNDLE_PATH}/MacOS/vcmiserver && cp ${CMAKE_HOME_DIRECTORY}/bin/$(CONFIGURATION)/libvcmi.dylib ${BUNDLE_PATH}/MacOS/libvcmi.dylib && cp ${CMAKE_HOME_DIRECTORY}/bin/$(CONFIGURATION)/libminizip.dylib ${BUNDLE_PATH}/MacOS/libminizip.dylib && cp ${CMAKE_HOME_DIRECTORY}/bin/$(CONFIGURATION)/libVCAI.dylib ${BUNDLE_PATH}/MacOS/AI/libVCAI.dylib && cp ${CMAKE_HOME_DIRECTORY}/bin/$(CONFIGURATION)/libStupidAI.dylib ${BUNDLE_PATH}/MacOS/AI/libStupidAI.dylib && cp ${CMAKE_HOME_DIRECTORY}/bin/$(CONFIGURATION)/libEmptyAI.dylib ${BUNDLE_PATH}/MacOS/AI/libEmptyAI.dylib && cp ${CMAKE_HOME_DIRECTORY}/bin/$(CONFIGURATION)/libBattleAI.dylib ${BUNDLE_PATH}/MacOS/AI/libBattleAI.dylib && cp -r ${CMAKE_HOME_DIRECTORY}/osx/vcmibuilder.app ${BUNDLE_PATH}/MacOS/vcmibuilder.app && # Copy frameworks cp -r ${CMAKE_HOME_DIRECTORY}/${CMAKE_FRAMEWORK_PATH} ${BUNDLE_PATH}/Frameworks/ && # Copy vcmi data mkdir -p ${BUNDLE_PATH}/Data && mkdir -p ${BUNDLE_PATH}/Data/Mods && mkdir -p ${BUNDLE_PATH}/Data/launcher && cp -r ${CMAKE_HOME_DIRECTORY}/config/ ${BUNDLE_PATH}/Data/config/ && cp -r ${CMAKE_HOME_DIRECTORY}/Mods/vcmi/ ${BUNDLE_PATH}/Data/Mods/vcmi/ && cp -r ${CMAKE_HOME_DIRECTORY}/Mods/WoG/ ${BUNDLE_PATH}/Data/Mods/WoG/ && cp -r ${CMAKE_HOME_DIRECTORY}/launcher/icons/ ${BUNDLE_PATH}/Data/launcher/icons/) add_custom_command(TARGET vcmiclient POST_BUILD COMMAND ${MakeVCMIBundle}) elseif(WIN32) add_executable(vcmiclient ${client_SRCS} VCMI_client.rc) else() add_executable(vcmiclient ${client_SRCS}) endif() if(WIN32) set_target_properties(vcmiclient PROPERTIES OUTPUT_NAME VCMI_client) endif() target_link_libraries(vcmiclient vcmi ${Boost_LIBRARIES} ${SDL_LIBRARY} ${SDLIMAGE_LIBRARY} ${SDLMIXER_LIBRARY} ${SDLTTF_LIBRARY} ${ZLIB_LIBRARIES} ${FFMPEG_LIBRARIES} ${SYSTEM_LIBS}) set_target_properties(vcmiclient PROPERTIES ${PCH_PROPERTIES}) cotire(vcmiclient) install(TARGETS vcmiclient DESTINATION ${BIN_DIR}) vcmi-0.98/client/CMessage.cpp000066400000000000000000000316171250671757600161110ustar00rootroot00000000000000/* * 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 "CDefHandler.h" #include "CGameInfo.h" #include "gui/SDL_Extensions.h" #include "../lib/CGeneralTextHandler.h" #include "Graphics.h" #include "windows/GUIClasses.h" #include "../lib/CConfigHandler.h" #include "CBitmapHandler.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: CComponent *comp; //blit component with image centered at this position void showAll(SDL_Surface * to); //ComponentResolved(); //c-tor ComponentResolved(CComponent *Comp); //c-tor ~ComponentResolved(); //d-tor }; // 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); //c-tor ~ComponentsToBlit(); //d-tor }; namespace { CDefHandler * ok = nullptr, *cancel = nullptr; std::vector > piecesOfBox; //in colors of all players SDL_Surface * background = nullptr; } void CMessage::init() { { piecesOfBox.resize(PlayerColor::PLAYER_LIMIT_I); for (int i=0; iourImages) { piecesOfBox[i].push_back(elem.bitmap); elem.bitmap->refcount++; } } for (auto & elem : bluePieces->ourImages) { graphics->blueToPlayersAdv(elem.bitmap, PlayerColor(i)); piecesOfBox[i].push_back(elem.bitmap); elem.bitmap->refcount++; } delete bluePieces; } background = BitmapHandler::loadBitmap("DIBOXBCK.BMP"); CSDL_Ext::setDefaultColorKey(background); } ok = CDefHandler::giveDef("IOKAY.DEF"); cancel = CDefHandler::giveDef("ICANCEL.DEF"); } void CMessage::dispose() { for (auto & piece : piecesOfBox) { for (auto & elem : piece) { SDL_FreeSurface(elem); } } SDL_FreeSurface(background); delete ok; delete cancel; } SDL_Surface * CMessage::drawDialogBox(int w, int h, PlayerColor playerColor) { //prepare surface SDL_Surface * ret = SDL_CreateRGBSurface(SDL_SWSURFACE, 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()) { // 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; winSize.second += 20 + //before button ok->ourImages[0].bitmap->h; //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) { std::vector &box = piecesOfBox[playerColor.getNum()]; // Note: this code assumes that the corner dimensions are all the same. // Horizontal borders int start_x = x + box[0]->w; const int stop_x = x + w - box[1]->w; const int bottom_y = y+h-box[7]->h+1; while (start_x < stop_x) { int cur_w = stop_x - start_x; if (cur_w > box[6]->w) cur_w = box[6]->w; // Top border Rect srcR(0, 0, cur_w, box[6]->h); Rect dstR(start_x, y, 0, 0); CSDL_Ext::blitSurface(box[6], &srcR, ret, &dstR); // Bottom border dstR.y = bottom_y; CSDL_Ext::blitSurface(box[7], &srcR, ret, &dstR); start_x += cur_w; } // Vertical borders int start_y = y + box[0]->h; const int stop_y = y + h - box[2]->h+1; const int right_x = x+w-box[5]->w; while (start_y < stop_y) { int cur_h = stop_y - start_y; if (cur_h > box[4]->h) cur_h = box[4]->h; // Left border Rect srcR(0, 0, box[4]->w, cur_h); Rect dstR(x, start_y, 0, 0); CSDL_Ext::blitSurface(box[4], &srcR, ret, &dstR); // Right border dstR.x = right_x; CSDL_Ext::blitSurface(box[5], &srcR, ret, &dstR); start_y += cur_h; } //corners Rect dstR(x, y, box[0]->w, box[0]->h); CSDL_Ext::blitSurface(box[0], nullptr, ret, &dstR); dstR=Rect(x+w-box[1]->w, y, box[1]->w, box[1]->h); CSDL_Ext::blitSurface(box[1], nullptr, ret, &dstR); dstR=Rect(x, y+h-box[2]->h+1, box[2]->w, box[2]->h); CSDL_Ext::blitSurface(box[2], nullptr, ret, &dstR); dstR=Rect(x+w-box[3]->w, y+h-box[3]->h+1, box[3]->w, box[3]->h); CSDL_Ext::blitSurface(box[3], nullptr, ret, &dstR); } ComponentResolved::ComponentResolved( CComponent *Comp ): comp(Comp) { //Temporary assign ownership on comp if (parent) parent->removeChild(this); if (comp->parent) { comp->parent->addChild(this); comp->parent->removeChild(comp); } addChild(comp); defActions = 255 - DISPOSE; pos.x = pos.y = 0; pos.w = comp->pos.w; pos.h = comp->pos.h; } ComponentResolved::~ComponentResolved() { if (parent) { removeChild(comp); parent->addChild(comp); } } void ComponentResolved::showAll(SDL_Surface *to) { CIntObject::showAll(to); comp->showAll(to); } ComponentsToBlit::~ComponentsToBlit() { for(auto & elem : comps) for(size_t j = 0; j < elem.size(); j++) delete elem[j]; } 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 = new ComponentResolved(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.98/client/CMessage.h000066400000000000000000000027421250671757600155530ustar00rootroot00000000000000#pragma once #include "Graphics.h" #include "gui/Geometries.h" /* * 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 * */ struct SDL_Surface; enum EWindowType {infoOnly, infoOK, yesOrNO}; class CInfoWindow; class CDefHandler; class CComponent; class CSelWindow; class ComponentResolved; /// Class which draws formatted text messages and generates chat windows class CMessage { public: //Function usd only in CMessage.cpp static std::pair getMaxSizes(std::vector > * txtg, int fontHeight); static SDL_Surface * blitTextOnSur(std::vector > * txtg, int fontHeight, int & curh, SDL_Surface * ret, int xCenterPos=-1); //xPos==-1 works as if ret->w/2 /// Draw border on exiting surface static void drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x=0, int y=0); /// Draw simple dialog box (borders and background only) static SDL_Surface * drawDialogBox(int w, int h, PlayerColor playerColor = PlayerColor(1)); 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.98/client/CMusicHandler.cpp000066400000000000000000000301161250671757600170740ustar00rootroot00000000000000#include "StdInc.h" #include #include "CMusicHandler.h" #include "CGameInfo.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" /* * 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 * */ #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(void) { CCS->musich->musicFinishedCallback(); } void CAudioBase::init() { if (initialized) return; if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1) { logGlobal->errorStream() << "Mix_OpenAudio error: " << 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"]) { 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 (initialized) { // Load sounds Mix_ChannelFinished(soundFinishedCallbackC); } } void CSoundHandler::release() { if (initialized) { Mix_HaltChannel(-1); for (auto &chunk : soundChunks) { if (chunk.second) Mix_FreeChunk(chunk.second); } } 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]; auto data = CResourceHandler::get()->load(ResourceID(std::string("SOUNDS/") + sound, EResType::SOUND))->readAll(); SDL_RWops *ops = SDL_RWFromMem(data.first.release(), data.second); Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops if (cache) soundChunks.insert(std::pair(sound, chunk)); return chunk; } catch(std::exception &e) { logGlobal->warnStream() << "Cannot get sound " << sound << " chunk: " << e.what(); return nullptr; } } // 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->traceStream() << "Attempt to play sound " << soundID << " with file name " << sound << " with cache"; 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->errorStream() << "Unable to play sound file " << sound << " , error " << 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) Mix_Volume(-1, (MIX_MAX_VOLUME * volume)/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); } void CMusicHandler::onVolumeChange(const JsonNode &volumeNode) { setVolume(volumeNode.Float()); } CMusicHandler::CMusicHandler(): listener(settings.listen["general"]["music"]) { listener(std::bind(&CMusicHandler::onVolumeChange, this, _1)); // Map music IDs // Vectors for helper const std::string setEnemy[] = {"AITheme0", "AITheme1", "AITheme2"}; const std::string setBattle[] = {"Combat01", "Combat02", "Combat03", "Combat04"}; auto fillSet = [=](std::string setName, const std::string list[], size_t amount) { for (size_t i=0; i < amount; i++) addEntryToSet(setName, i, "music/" + list[i]); }; fillSet("enemy-turn", setEnemy, ARRAY_COUNT(setEnemy)); fillSet("battle", setBattle, ARRAY_COUNT(setBattle)); 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(musicMutex); 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->errorStream() << "Error: playing music from non-existing set: " << 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->errorStream() << "Error: playing music from non-existing set: " << whichSet; return; } auto selectedEntry = selectedSet->second.find(entryID); if (selectedEntry == selectedSet->second.end()) { logGlobal->errorStream() << "Error: playing non-existing entry " << entryID << " from set: " << 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(unique_ptr queued) { if (!initialized) return; boost::mutex::scoped_lock guard(musicMutex); 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->errorStream() << "Failed to queue music. setName=" << setName << "\tmusicURI=" << musicURI; logGlobal->errorStream() << "Exception: " << e.what(); } } void CMusicHandler::stopMusic(int fade_ms) { if (!initialized) return; boost::mutex::scoped_lock guard(musicMutex); 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(void) { boost::mutex::scoped_lock guard(musicMutex); 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), musicFile(nullptr), loop(looped ? -1 : 1), setName(setName) { if (!musicURI.empty()) load(musicURI); } MusicEntry::~MusicEntry() { logGlobal->traceStream()<<"Del-ing music file "<traceStream()<<"Del-ing music file "<traceStream()<<"Loading music file "<load(ResourceID(musicURI, EResType::MUSIC))->readAll(); musicFile = SDL_RWFromConstMem(data.first.get(), data.second); #ifdef VCMI_SDL1 music = Mix_LoadMUS_RW(musicFile); if(!music) { SDL_FreeRW(musicFile); musicFile = nullptr; logGlobal->warnStream() << "Warning: Cannot open " << currentName << ": " << Mix_GetError(); return; } #else music = Mix_LoadMUS_RW(musicFile, SDL_FALSE); if(!music) { SDL_FreeRW(musicFile); musicFile = nullptr; logGlobal->warnStream() << "Warning: Cannot open " << currentName << ": " << Mix_GetError(); return; } #endif // 0 } 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->traceStream()<<"Playing music file "<errorStream() << "Unable to play music (" << Mix_GetError() << ")"; return false; } return true; } bool MusicEntry::stop(int fade_ms) { if (Mix_PlayingMusic()) { logGlobal->traceStream()<<"Stoping music file "< 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; public: CSoundHandler(); void init(); void release(); void setVolume(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); // 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 { std::pair, size_t> data; CMusicHandler *owner; Mix_Music *music; SDL_RWops *musicFile; 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: // Because we use the SDL music callback, our music variables must // be protected boost::mutex musicMutex; //update volume on configuration change SettingsListener listener; void onVolumeChange(const JsonNode &volumeNode); unique_ptr current; unique_ptr next; void queueNext(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped); void queueNext(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(); void release(); void setVolume(ui32 percent); /// 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(void); friend class MusicEntry; }; vcmi-0.98/client/CPlayerInterface.cpp000066400000000000000000002676131250671757600176110ustar00rootroot00000000000000#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 "../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/Connection.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/BattleState.h" #include "../lib/JsonNode.h" #include "CMusicHandler.h" #include "../lib/CondSh.h" #include "../lib/NetPacks.h" #include "../lib/mapping/CMap.h" #include "../lib/VCMIDirs.h" #include "mapHandler.h" #include "../lib/CStopWatch.h" #include "../lib/StartInfo.h" #include "../lib/CGameState.h" #include "../lib/GameConstants.h" #include "gui/CGuiHandler.h" #include "windows/InfoWindows.h" #include "../lib/UnlockGuard.h" #include /* * 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 * */ // 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; CondSh CPlayerInterface::terminate_cond; CPlayerInterface * LOCPLINT; CBattleInterface * CPlayerInterface::battleInt; enum EMoveState {STOP_MOVE, WAITING_MOVE, CONTINUE_MOVE, DURING_MOVE}; CondSh stillMoveHero; //used during hero movement int CPlayerInterface::howManyPeople = 0; static bool objectBlitOrderSorter(const TerrainTileObject & a, const TerrainTileObject & b) { return CMapHandler::compareObjectBlitOrder(a.obj, b.obj); } CPlayerInterface::CPlayerInterface(PlayerColor Player) { logGlobal->traceStream() << "\tHuman player interface for player " << Player << " being constructed"; destinationTeleport = ObjectInstanceID(); observerInDuelMode = false; howManyPeople++; GH.defActionsDef = 0; LOCPLINT = this; curAction = nullptr; playerID=Player; human=true; currentSelection = nullptr; castleInt = nullptr; battleInt = nullptr; //pim = new boost::recursive_mutex; makingTurn = false; showingDialog = new CondSh(false); cingconsole = new CInGameConsole; terminate_cond.set(false); firstCall = 1; //if loading will be overwritten in serialize autosaveCount = 0; isAutoFightOn = false; duringMovement = false; ignoreEvents = false; locked = false; } CPlayerInterface::~CPlayerInterface() { logGlobal->traceStream() << "\tHuman player interface for player " << playerID << " being destructed"; //howManyPeople--; //delete pim; //vstd::clear_pointer(pim); delete showingDialog; delete cingconsole; if(LOCPLINT == this) LOCPLINT = nullptr; } void CPlayerInterface::init(shared_ptr CB) { cb = CB; if(observerInDuelMode) return; if(!towns.size() && !wanderingHeroes.size()) initializeHeroTownList(); // always recreate advmap interface to avoid possible memory-corruption bugs if(adventureInt) delete adventureInt; adventureInt = 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; if(firstCall) { if(howManyPeople == 1) adventureInt->setPlayer(playerID); autosaveCount = getLastIndex("Autosave_"); if(firstCall > 0) //new game, not loaded { int index = getLastIndex("Newgame_Autosave_"); index %= SAVES_COUNT; cb->save("Saves/Newgame_Autosave_" + boost::lexical_cast(index + 1)); } firstCall = 0; } else { LOCPLINT->cb->save("Saves/Autosave_" + boost::lexical_cast(autosaveCount++ + 1)); autosaveCount %= 5; } if(adventureInt->player != playerID) adventureInt->setPlayer(playerID); if(howManyPeople > 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(new CComponent(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; 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 { //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.result != TryMoveHero::SUCCESS) //hero failed to move { hero->isStanding = true; stillMoveHero.setn(STOP_MOVE); GH.totalRedraw(); adventureInt->heroList.update(hero); return; } ui32 speed; 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); adventureInt->updateScreen = true; adventureInt->show(screen); { //evil returns here ... //todo: get rid of it logGlobal->traceStream() << "before [un]locks in " << __FUNCTION__; auto unlockPim = vstd::makeUnlockGuard(*pim); //let frame to be rendered GH.mainFPSmng->framerateDelay(); //for animation purposes logGlobal->traceStream() << "after [un]locks in " << __FUNCTION__; } //CSDL_Ext::update(screen); } //for(int i=1; i<32; i+=4) //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::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 = new CCastleInterface(town); GH.pushInt(castleInt); } 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::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) { EVENT_HANDLER_CALLED_BY_CLIENT; if(which == 4) { if(CAltarWindow *ctw = dynamic_cast(GH.topInt())) 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()); 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(int type, int val) { EVENT_HANDLER_CALLED_BY_CLIENT; if(CMarketplaceWindow *mw = dynamic_cast(GH.topInt())) mw->resourceChanged(type, val); 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); CLevelWindow *lw = new CLevelWindow(hero,pskill,skills, [=](ui32 selection){ cb->selectionMade(selection, queryID); }); GH.pushInt(lw); } void CPlayerInterface::commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); CCS->soundh->playSound(soundBase::heroNewLevel); GH.pushInt(new CStackWindow(commander, skills, [=](ui32 selection) { cb->selectionMade(selection, queryID); })); } void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) { EVENT_HANDLER_CALLED_BY_CLIENT; updateInfo(town); if(town->garrisonHero && vstd::contains(wanderingHeroes,town->garrisonHero)) //wandering hero moved to the garrison { CGI->mh->hideObject(town->garrisonHero); if (town->garrisonHero->tempOwner == playerID) // our hero wanderingHeroes -= town->garrisonHero; } if(town->visitingHero && !vstd::contains(wanderingHeroes,town->visitingHero)) //hero leaves garrison { CGI->mh->printObject(town->visitingHero); if (town->visitingHero->tempOwner == playerID) // our hero wanderingHeroes.push_back(town->visitingHero); } adventureInt->heroList.update(); adventureInt->updateNextHero(nullptr); if(CCastleInterface *c = castleInt) { c->garr->selectSlot(nullptr); c->garr->setArmy(town->getUpperArmy(), 0); c->garr->setArmy(town->visitingHero, 1); c->garr->recreateSlots(); c->heroes->update(); } for(IShowActivatable *isa : GH.listInt) { CKingdomInterface *ki = dynamic_cast(isa); 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(std::vector objs) { boost::unique_lock un(*pim); for(auto object : objs) updateInfo(object); for(auto & elem : GH.listInt) { CGarrisonHolder *cgh = dynamic_cast(elem); if (cgh) cgh->updateGarrisons(); if(CTradeWindow *cmw = dynamic_cast(elem)) { 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) return; if(castleInt->town!=town) return; switch(what) { case 1: CCS->soundh->playSound(soundBase::newBuilding); castleInt->addBuilding(buildingID); break; case 2: castleInt->removeBuilding(buildingID); break; } adventureInt->townList.update(town); castleInt->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"]["neutralAI"].String()); autofightingAI->init(cb); autofightingAI->battleStart(army1, army2, int3(0,0,0), hero1, hero2, side); isAutoFightOn = true; cb->registerBattleInterface(autofightingAI); } //Don't wait for dialogs when we are non-active hot-seat player if(LOCPLINT == this) waitForAllDialogs(); BATTLE_EVENT_POSSIBLE_RETURN; } void CPlayerInterface::battleStacksHealedRes(const std::vector > & healedStacks, bool lifeDrain, bool tentHeal, si32 lifeDrainFrom) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; for(auto & healedStack : healedStacks) { const CStack * healed = cb->battleGetStackByID(healedStack.first); if(battleInt->creAnims[healed->ID]->isDead()) { //stack has been resurrected battleInt->creAnims[healed->ID]->setType(CCreatureAnim::HOLDING); } } if (lifeDrain) { const CStack *attacker = cb->battleGetStackByID(healedStacks[0].first, false); const CStack *defender = cb->battleGetStackByID(lifeDrainFrom, false); int textOff = 0; if (attacker) { battleInt->displayEffect(52, attacker->position); //TODO: transparency if (attacker->count > 1) { textOff += 1; } CCS->soundh->playSound(soundBase::DRAINLIF); //print info about life drain auto txt = boost::format (CGI->generaltexth->allTexts[361 + textOff]) % attacker->getCreature()->nameSing % healedStacks[0].second % defender->getCreature()->namePl; battleInt->console->addText(boost::to_string(txt)); } } if (tentHeal) { std::string text = CGI->generaltexth->allTexts[414]; boost::algorithm::replace_first(text, "%s", cb->battleGetStackByID(lifeDrainFrom, false)->getCreature()->nameSing); boost::algorithm::replace_first(text, "%s", cb->battleGetStackByID(healedStacks[0].first, false)->getCreature()->nameSing); boost::algorithm::replace_first(text, "%d", boost::lexical_cast(healedStacks[0].second)); battleInt->console->addText(text); } } void CPlayerInterface::battleNewStackAppeared(const CStack * stack) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->newStack(stack); } void CPlayerInterface::battleObstaclesRemoved(const std::set & removedObstacles) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; // for(std::set::const_iterator it = removedObstacles.begin(); it != removedObstacles.end(); ++it) // { // for(std::map< int, CDefHandler * >::iterator itBat = battleInt->idToObstacle.begin(); itBat != battleInt->idToObstacle.end(); ++itBat) // { // if(itBat->first == *it) //remove this obstacle // { // battleInt->idToObstacle.erase(itBat); // break; // } // } // } //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::battleStacksRemoved(const BattleStacksRemoved & bsr) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; for(auto & elem : bsr.stackIDs) //for each removed stack { battleInt->stackRemoved(elem); } } 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->traceStream() << "Awaiting command for " << stack->nodeName(); if(autofightingAI) { if(isAutoFightOn) { auto ret = autofightingAI->activeStack(stack); if(isAutoFightOn) { return ret; } } cb->unregisterBattleInterface(autofightingAI); autofightingAI.reset(); } CBattleInterface *b = battleInt; if (b->givenCommand->get()) { logGlobal->errorStream() << "Command buffer must be clean! (we don't want to use old command)"; vstd::clear_pointer(b->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(b->givenCommand->mx); while(!b->givenCommand->data) { b->givenCommand->cond.wait(lock); if(!battleInt) //batle ended while we were waiting for movement (eg. because of spell) throw boost::thread_interrupted(); //will shut the thread peacefully } //tidy up BattleAction ret = *(b->givenCommand->data); delete b->givenCommand->data; b->givenCommand->data = nullptr; //return command logGlobal->traceStream() << "Giving command for " << stack->nodeName(); return ret; } void CPlayerInterface::battleEnd(const BattleResult *br) { EVENT_HANDLER_CALLED_BY_CLIENT; if(isAutoFightOn) { isAutoFightOn = false; cb->unregisterBattleInterface(autofightingAI); autofightingAI.reset(); if(!battleInt) { SDL_Rect temp_rect = genRect(561, 470, (screen->w - 800)/2 + 165, (screen->h - 600)/2 + 19); auto resWindow = new CBattleResultWindow(*br, temp_rect, *this); GH.pushInt(resWindow); // #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) { 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->position); } if(elem.isSpell()) { if (defender) battleInt->displaySpellEffect(elem.spellID, defender->position); } //FIXME: why action is deleted during enchanter cast? bool remoteAttack = false; if (LOCPLINT->curAction) remoteAttack |= LOCPLINT->curAction->actionType != Battle::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); } void CPlayerInterface::battleAttack(const BattleAttack *ba) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; assert(curAction); if(ba->lucky()) //lucky hit { const CStack *stack = cb->battleGetStackByID(ba->stackAttacking); std::string hlp = CGI->generaltexth->allTexts[45]; boost::algorithm::replace_first(hlp,"%s", (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str()); battleInt->console->addText(hlp); battleInt->displayEffect(18, stack->position); CCS->soundh->playSound(soundBase::GOODLUCK); } if(ba->unlucky()) //unlucky hit { const CStack *stack = cb->battleGetStackByID(ba->stackAttacking); std::string hlp = CGI->generaltexth->allTexts[44]; boost::algorithm::replace_first(hlp,"%s", (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str()); battleInt->console->addText(hlp); battleInt->displayEffect(48, stack->position); CCS->soundh->playSound(soundBase::BADLUCK); } if (ba->deathBlow()) { const CStack *stack = cb->battleGetStackByID(ba->stackAttacking); std::string hlp = CGI->generaltexth->allTexts[(stack->count != 1) ? 366 : 365]; boost::algorithm::replace_first(hlp,"%s", (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str()); battleInt->console->addText(hlp); for (auto & elem : ba->bsa) { const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked); battleInt->displayEffect(73, attacked->position); } CCS->soundh->playSound(soundBase::deathBlow); } battleInt->waitForAnims(); const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking); 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->position, attacked, true); } } } else { int shift = 0; if(ba->counter() && BattleHex::mutualPosition(curAction->destinationTile, attacker->position) < 0) { int distp = BattleHex::getDistance(curAction->destinationTile + 1, attacker->position); int distm = BattleHex::getDistance(curAction->destinationTile - 1, attacker->position); if( distp < distm ) shift = 1; else shift = -1; } const CStack * attacked = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked); battleInt->stackAttacking( attacker, ba->counter() ? curAction->destinationTile + shift : curAction->additionalInfo, attacked, false); } //battleInt->waitForAnims(); //FIXME: freeze if(ba->spellLike()) { //display hit animation SpellID spellID = ba->spellID; battleInt->displaySpellHit(spellID,curAction->destinationTile); } } void CPlayerInterface::battleObstaclePlaced(const CObstacleInstance &obstacle) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->obstaclePlaced(obstacle); } 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(new CComponent(*component)); showInfoDialog(text,intComps,soundID); } void CPlayerInterface::showInfoDialog(const std::string &text, CComponent * component) { std::vector intComps; intComps.push_back(component); showInfoDialog(text, intComps, soundBase::sound_todo, true); } void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector & components, int soundID, bool delComps) { LOG_TRACE_PARAMS(logGlobal, "player=%s, text=%s, is LOCPLINT=%d", playerID % text % (this==LOCPLINT)); waitWhileDialog(); if (settings["session"]["autoSkip"].Bool() && !LOCPLINT->shiftPressed()) { return; } CInfoWindow *temp = CInfoWindow::create(text, playerID, &components); temp->setDelComps(delComps); 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::vector comps; for(auto & elem : components) { comps.push_back(&elem); } std::string str; text.toString(str); showInfoDialog(str,comps, 0); waitWhileDialog(); } void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, bool DelComps, const std::vector & components) { boost::unique_lock un(*pim); stopMovement(); LOCPLINT->showingDialog->setn(true); CInfoWindow::showYesNoDialog(text, &components, onYes, onNo, DelComps, playerID); } void CPlayerInterface::showOkDialog(std::vector & components, const MetaString & text, const std::function & onOk) { boost::unique_lock un(*pim); std::vector comps; for(auto & elem : components) { comps.push_back(&elem); } std::string str; text.toString(str); stopMovement(); showingDialog->setn(true); std::vector intComps; for(auto & component : comps) intComps.push_back(new CComponent(*component)); CInfoWindow::showOkDialog(str, &intComps, onOk, true, playerID); } void CPlayerInterface::showBlockingDialog( const std::string &text, const std::vector &components, QueryID askID, 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(new CComponent(component)); //will be deleted by close in window showYesNoDialog(text, [=]{ cb->selectionMade(1, askID); }, [=]{ cb->selectionMade(0, askID); }, true, intComps); } else if(selection) { std::vector intComps; for(auto & component : components) intComps.push_back(new CSelectableComponent(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; auto temp = new CSelWindow(text, playerID, charperline, intComps, pom, askID); GH.pushInt(temp); intComps[0]->clickLeft(true, false); } } void CPlayerInterface::showTeleportDialog(TeleportChannelID channel, std::vector exits, bool impassable, QueryID askID) { EVENT_HANDLER_CALLED_BY_CLIENT; ObjectInstanceID choosenExit; if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, destinationTeleport)) choosenExit = destinationTeleport; cb->selectionMade(choosenExit.getNum(), askID); } 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.pushInt(new CHeroWindow(hero)); } /* void CPlayerInterface::heroArtifactSetChanged(const CGHeroInstance*hero) { boost::unique_lock un(*pim); if(adventureInt->heroWindow->curHero && adventureInt->heroWindow->curHero->id == hero->id) //hero window is opened { adventureInt->heroWindow->deactivate(); adventureInt->heroWindow->setHero(hero); adventureInt->heroWindow->activate(); } else if(CExchangeWindow* cew = dynamic_cast(GH.topInt())) //exchange window is open { cew->deactivate(); for(int g=0; gheroInst); ++g) { if(cew->heroInst[g]->id == hero->id) { cew->heroInst[g] = hero; cew->artifs[g]->updateState = true; cew->artifs[g]->setHero(hero); cew->artifs[g]->updateState = false; } } cew->prepareBackground(); cew->activate(); } else if(CTradeWindow *caw = dynamic_cast(GH.topInt())) { if(caw->arts) { caw->deactivate(); caw->arts->updateState = true; caw->arts->setHero(hero); caw->arts->updateState = false; caw->activate(); } } updateInfo(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()); if(fs) fs->creaturesChanged(); for(IShowActivatable *isa : GH.listInt) { CKingdomInterface *ki = dynamic_cast(isa); 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()); 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 ) { h & observerInDuelMode; h & wanderingHeroes & towns & 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->errorStream() << p.first->name << " has assigned an empty path! Ignoring it..."; } h & pathsMap; } else { h & pathsMap; if(cb) for(auto &p : pathsMap) { CGPath path; cb->getPathsInfo(p.first)->getPath(p.second, path); paths[p.first] = path; logGlobal->traceStream() << boost::format("Restored path for hero %s leading to %s with %d nodes") % p.first->nodeName() % p.second % path.nodes.size(); } } h & spellbookSettings; } void CPlayerInterface::saveGame( COSer & h, const int version ) { EVENT_HANDLER_CALLED_BY_CLIENT; serializeTempl(h,version); } void CPlayerInterface::loadGame( CISer & h, const int version ) { EVENT_HANDLER_CALLED_BY_CLIENT; serializeTempl(h,version); firstCall = -1; } void CPlayerInterface::moveHero( const CGHeroInstance *h, CGPath path ) { logGlobal->traceStream() << __FUNCTION__; 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; duringMovement = 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 = new CGarrisonWindow(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 = new CComponent(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, true, 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(); stillMoveHero.setn(CONTINUE_MOVE); } } void CPlayerInterface::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) { EVENT_HANDLER_CALLED_BY_CLIENT; GH.pushInt(new CExchangeWindow(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() { std::vector allHeroes = cb->getHeroesInfo(); /* std::vector newWanderingHeroes; //applying current heroes order to new heroes info int j; for (int i = 0; i < wanderingHeroes.size(); i++) if ((j = vstd::find_pos(allHeroes, wanderingHeroes[i])) >= 0) if (!allHeroes[j]->inTownGarrison) { newWanderingHeroes += allHeroes[j]; allHeroes -= allHeroes[j]; } //all the rest of new heroes go the end of the list wanderingHeroes.clear(); wanderingHeroes = newWanderingHeroes; newWanderingHeroes.clear();*/ for (auto & allHeroe : allHeroes) if (!allHeroe->inTownGarrison) wanderingHeroes.push_back(allHeroe); std::vector allTowns = cb->getTownsInfo(); /* std::vector newTowns; for (int i = 0; i < towns.size(); i++) if ((j = vstd::find_pos(allTowns, towns[i])) >= 0) { newTowns += allTowns[j]; allTowns -= allTowns[j]; } towns.clear(); towns = newTowns; newTowns.clear();*/ for(auto & allTown : allTowns) towns.push_back(allTown); 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); }; CRecruitmentWindow *cr = new CRecruitmentWindow(dwelling, level, dst, recruitCb); GH.pushInt(cr); } void CPlayerInterface::waitWhileDialog(bool unlockPim /*= true*/) { if(GH.amIGuiThread()) { logGlobal->warnStream() << "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); CShipyardWindow *csw = new CShipyardWindow(cost, state, obj->getBoatType(), [=]{ cb->buildBoat(obj); }); GH.pushInt(csw); } 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(); adventureInt->centerOn (pos); if(focusTime) { GH.totalRedraw(); #ifdef VCMI_SDL1 CSDL_Ext::update(screen); SDL_Delay(focusTime); #else { auto unlockPim = vstd::makeUnlockGuard(*pim); IgnoreEvents ignore(*this); SDL_Delay(focusTime); } #endif } } void CPlayerInterface::objectRemoved( const CGObjectInstance *obj ) { EVENT_HANDLER_CALLED_BY_CLIENT; if (LOCPLINT->cb->getCurrentPlayer() == playerID) { std::string handlerName = VLC->objtypeh->getObjectHandlerName(obj->ID); if ((handlerName == "pickable") || (handlerName == "scholar") || (handlerName== "artifact") || (handlerName == "pandora")) { waitWhileDialog(); CCS->soundh->playSoundFromSet(CCS->soundh->pickupSounds); } else if ((handlerName == "monster") || (handlerName == "hero")) { waitWhileDialog(); CCS->soundh->playSound(soundBase::KillFade); } } 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; } void CPlayerInterface::update() { if (!locked) { logGlobal->errorStream() << "Non synchronized update of PlayerInterface"; return; } //if there are any waiting dialogs, show them if((howManyPeople <= 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 && !adventureInt->selection && GH.topInt() == adventureInt) { return; } // Handles mouse and key input GH.updateTime(); GH.handleEvents(); if(adventureInt && !adventureInt->isActive() && adventureInt->scrollingDir) //player forces map scrolling though interface is disabled GH.totalRedraw(); else GH.simpleRedraw(); if (settings["general"]["showfps"].Bool()) GH.drawFPSCounter(); } void CPlayerInterface::runLocked(std::function functor) { // Updating GUI requires locking pim mutex (that protects screen and GUI state). // 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. bool acquiredTheLockOnPim = false; //for tracking whether pim mutex locking succeeded while(!terminate_cond.get() && !(acquiredTheLockOnPim = 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) { // We broke the while loop above and not because of mutex, so we must be terminating. assert(terminate_cond.get()); return; } // If we are here, pim mutex has been successfully locked - let's store it in a safe RAII lock. boost::unique_lock un(*pim, boost::adopt_lock); // While mutexes were locked away we may be have stopped being the active interface if(LOCPLINT != this) return; // Make sure that gamestate won't change when GUI objects may obtain its parts on event processing or drawing request boost::shared_lock gsLock(cb->getGsMutex()); locked = true; functor(); locked = false; } 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 } --howManyPeople; if(howManyPeople == 0) //all human players eliminated { if(adventureInt) { terminate_cond.setn(true); adventureInt->deactivate(); if(GH.topInt() == adventureInt) GH.popInt(adventureInt); delete adventureInt; adventureInt = nullptr; } } if(cb->getStartInfo()->mode == StartInfo::CAMPAIGN) { // if you lose the campaign go back to the main menu // campaign wins are handled in proposeNextMission if(victoryLossCheckResult.loss()) requestReturningToMainMenu(); } else { if(howManyPeople == 0) //all human players eliminated { requestReturningToMainMenu(); } else if(victoryLossCheckResult.victory() && LOCPLINT == this) // end game if current human player has won { requestReturningToMainMenu(); } } 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, new CComponent(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.pushInt(new CPuzzleWindow(grailPos, ratio)); } void CPlayerInterface::viewWorldMap() { adventureInt->changeMode(EAdvMapMode::WORLD_VIEW); } void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, int spellID) { EVENT_HANDLER_CALLED_BY_CLIENT; if (spellID == SpellID::FLY || spellID == SpellID::WATER_WALK) { eraseCurrentPathOf(caster, false); } const CSpell * spell = CGI->spellh->objects[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 /*= true */ ) { 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->warnStream() << "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.endPos(), path)) return &path; else paths.erase(h); } } return nullptr; } void CPlayerInterface::acceptTurn() { if(settings["session"]["autoSkip"].Bool()) { while(CInfoWindow *iw = dynamic_cast(GH.topInt())) iw->close(); } waitWhileDialog(); if(howManyPeople > 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); } else adventureInt->select(towns.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())) 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; auto daysWithoutCastle = *cb->getPlayer(playerColor)->daysWithoutCastle; 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); } } void CPlayerInterface::tryDiggging(const CGHeroInstance *h) { std::string hlp; CGI->mh->getTerrainDescr(h->getPosition(false), hlp, false); int msgToShow = -1; CGHeroInstance::ECanDig isDiggingPossible = h->diggingStatus(); if(hlp.length()) isDiggingPossible = CGHeroInstance::TILE_OCCUPIED; //TODO integrate with canDig switch(isDiggingPossible) { case CGHeroInstance::CAN_DIG: break; case CGHeroInstance::LACK_OF_MOVEMENT: msgToShow = 56; //"Digging for artifacts requires a whole day, try again tomorrow." break; case CGHeroInstance::TILE_OCCUPIED: msgToShow = 97; //Try searching on clear ground. break; case CGHeroInstance::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.pushInt(new CAltarWindow(market, visitor, EMarketMode::ARTIFACT_EXP)); else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD) GH.pushInt(new CAltarWindow(market, visitor, EMarketMode::CREATURE_EXP)); } else GH.pushInt(new CMarketplaceWindow(market, visitor, market->availableModes().front())); } void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) { EVENT_HANDLER_CALLED_BY_CLIENT; auto cuw = new CUniversityWindow(visitor, market); GH.pushInt(cuw); } void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) { EVENT_HANDLER_CALLED_BY_CLIENT; auto chfw = new CHillFortWindow(visitor, object); GH.pushInt(chfw); } void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket *bm /*= nullptr*/) { EVENT_HANDLER_CALLED_BY_CLIENT; if(CMarketplaceWindow *cmw = dynamic_cast(GH.topInt())) cmw->artifactsChanged(false); } void CPlayerInterface::showTavernWindow(const CGObjectInstance *townOrTavern) { EVENT_HANDLER_CALLED_BY_CLIENT; auto tv = new CTavernWindow(townOrTavern); GH.pushInt(tv); } void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj) { EVENT_HANDLER_CALLED_BY_CLIENT; auto tgw = new CThievesGuildWindow(obj); GH.pushInt(tgw); } void CPlayerInterface::showQuestLog() { EVENT_HANDLER_CALLED_BY_CLIENT; CQuestLog * ql = new CQuestLog (LOCPLINT->cb->getMyQuests()); GH.pushInt (ql); } 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() { sendCustomEvent(RETURN_TO_MAIN_MENU); cb->unregisterAllInterfaces(); } void CPlayerInterface::requestStoppingClient() { sendCustomEvent(STOP_CLIENT); } void CPlayerInterface::sendCustomEvent( int code ) { CGuiHandler::pushSDLEvent(SDL_USEREVENT, code); } void CPlayerInterface::stackChagedCount(const StackLocation &location, const TQuantity &change, bool isAbsolute) { EVENT_HANDLER_CALLED_BY_CLIENT; garrisonChanged(location.army); } void CPlayerInterface::stackChangedType(const StackLocation &location, const CCreature &newType) { EVENT_HANDLER_CALLED_BY_CLIENT; garrisonChanged(location.army); } void CPlayerInterface::stacksErased(const StackLocation &location) { EVENT_HANDLER_CALLED_BY_CLIENT; garrisonChanged(location.army); } void CPlayerInterface::stacksSwapped(const StackLocation &loc1, const StackLocation &loc2) { EVENT_HANDLER_CALLED_BY_CLIENT; std::vector objects; objects.push_back(loc1.army); if(loc2.army != loc1.army) objects.push_back(loc2.army); garrisonsChanged(objects); } void CPlayerInterface::newStackInserted(const StackLocation &location, const CStackInstance &stack) { EVENT_HANDLER_CALLED_BY_CLIENT; garrisonChanged(location.army); } void CPlayerInterface::stacksRebalanced(const StackLocation &src, const StackLocation &dst, TQuantity count) { EVENT_HANDLER_CALLED_BY_CLIENT; std::vector objects; objects.push_back(src.army); if(src.army != dst.army) objects.push_back(dst.army); garrisonsChanged(objects); } void CPlayerInterface::artifactPut(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; } void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); for(IShowActivatable *isa : GH.listInt) { auto artWin = dynamic_cast(isa); if(artWin) artWin->artifactRemoved(al); } } void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); for(IShowActivatable *isa : GH.listInt) { auto artWin = dynamic_cast(isa); if(artWin) artWin->artifactMoved(src, dst); } } void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); for(IShowActivatable *isa : GH.listInt) { auto artWin = dynamic_cast(isa); if(artWin) artWin->artifactAssembled(al); } } void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); for(IShowActivatable *isa : GH.listInt) { auto artWin = dynamic_cast(isa); if(artWin) artWin->artifactDisassembled(al); } } void CPlayerInterface::playerStartsTurn(PlayerColor player) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->infoBar.showSelection(); if (!vstd::contains (GH.listInt, adventureInt)) { GH.popInts (GH.listInt.size()); //after map load - remove everything else GH.pushInt (adventureInt); } else { while (GH.listInt.front() != adventureInt && !dynamic_cast(GH.listInt.front())) //don't remove dialogs that expect query answer GH.popInts(1); } if(howManyPeople == 1) { GH.curInt = this; adventureInt->startTurn(); } if(player != playerID && this == LOCPLINT) { waitWhileDialog(); adventureInt->aiTurnStarted(); } } void CPlayerInterface::waitForAllDialogs(bool unlockPim /*= true*/) { while(!dialogs.empty()) { auto unlock = vstd::makeUnlockGuardIf(*pim, unlockPim); SDL_Delay(5); } waitWhileDialog(unlockPim); } void CPlayerInterface::proposeLoadingGame() { showYesNoDialog(CGI->generaltexth->allTexts[68], [this] { sendCustomEvent(RETURN_TO_MENU_LOAD); }, 0, false); } 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::doMoveHero(const CGHeroInstance * h, CGPath path) { int i = 1; auto getObj = [&](int3 coord, bool ignoreHero) { return cb->getTile(CGHeroInstance::convertPosition(coord,false))->topVisitableObj(ignoreHero); }; 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; for(i=path.nodes.size()-1; i>0 && (stillMoveHero.data == CONTINUE_MOVE); i--) { int3 currentCoord = path.nodes[i].coord; int3 nextCoord = path.nodes[i-1].coord; auto nextObject = getObj(nextCoord, nextCoord == h->pos); if(CGTeleport::isConnected(getObj(currentCoord, currentCoord == h->pos), nextObject)) { CCS->soundh->stopSound(sh); destinationTeleport = nextObject->id; doMovement(h->pos, false); 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->traceStream() << "Requesting hero movement to " << endpos; if((i-2 >= 0) // Check there is node after next one; otherwise transit is pointless && (CGTeleport::isConnected(nextObject, getObj(path.nodes[i-2].coord, false)) || CGTeleport::isTeleport(nextObject))) { // Hero should be able to go through object if it's allow transit doMovement(endpos, true); } else doMovement(endpos, false); logGlobal->traceStream() << "Resuming " << __FUNCTION__; bool guarded = cb->isInTheMap(cb->getGuardingCreaturePosition(endpos - int3(1, 0, 0))); if(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); } duringMovement = 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(); } vcmi-0.98/client/CPlayerInterface.h000066400000000000000000000367131250671757600172510ustar00rootroot00000000000000#pragma once //#include "../lib/CondSh.h" #include "../lib/FunctionList.h" #include "../lib/CGameInterface.h" #include "../lib/NetPacksBase.h" #include "gui/CIntObject.h" //#include "../lib/CGameState.h" #ifdef __GNUC__ #define sprintf_s snprintf #endif #ifdef max #undef max #endif #ifdef min #undef min #endif /* * 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 * */ class CDefEssential; class CButton; class CToggleGroup; class CDefHandler; struct TryMoveHero; class CDefEssential; 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 CGarrisonInt; 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; } enum { /*CHANGE_SCREEN_RESOLUTION = 1,*/ RETURN_TO_MAIN_MENU = 2, STOP_CLIENT = 3, RESTART_GAME, RETURN_TO_MENU_LOAD, FULLSCREEN_TOGGLED, PREPARE_RESTART_CAMPAIGN }; /// Central class for managing user interface logic class CPlayerInterface : public CGameInterface, public ILockedUpdatable { const CArmedInstance * currentSelection; public: bool observerInDuelMode; ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation //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; static int howManyPeople; CCastleInterface * castleInt; //nullptr if castle window isn't opened static CBattleInterface * battleInt; //nullptr if no battle CInGameConsole * cingconsole; 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 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 & spellbokLastPageAdvmap & spellbookLastTabBattle & spellbookLastTabAdvmap; } } spellbookSettings; void update() override; void runLocked(std::function functor) override; void initializeHeroTownList(); int getLastIndex(std::string namePrefix); //overridden funcs from CGameInterface void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; //what: 1 - built, 2 - demolished void stackChagedCount(const StackLocation &location, const TQuantity &change, bool isAbsolute) override; //if absolute, change is the new count; otherwise count was modified by adding change void stackChangedType(const StackLocation &location, const CCreature &newType) override; //used eg. when upgrading creatures void stacksErased(const StackLocation &location) override; //stack removed from previously filled slot void stacksSwapped(const StackLocation &loc1, const StackLocation &loc2) override; void newStackInserted(const StackLocation &location, const CStackInstance &stack) override; //new stack inserted at given (previously empty position) void stacksRebalanced(const StackLocation &src, const StackLocation &dst, TQuantity count) override; //moves creatures from src stack to dst slot, may be used for merging/splittint/moving stacks void artifactPut(const ArtifactLocation &al); void artifactRemoved(const ArtifactLocation &al); void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst); void artifactAssembled(const ArtifactLocation &al); void artifactDisassembled(const ArtifactLocation &al); 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(int type, int val) 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, 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, std::vector exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) 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(COSer & h, const int version) override; //saving void loadGame(CISer & 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) 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 battleStacksHealedRes(const std::vector > & healedStacks, bool lifeDrain, bool tentHeal, si32 lifeDrainFrom) override; //called when stacks are healed / resurrected void battleNewStackAppeared(const CStack * stack) override; //not called at the beginning of a battle or by resurrection; called eg. when elemental is summoned void battleObstaclesRemoved(const std::set & removedObstacles) override; //called when a certain set of obstacles is removed from batlefield; IDs of them are given void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleStacksRemoved(const BattleStacksRemoved & bsr) override; //called when certain stack is completely removed from battlefield void battleObstaclePlaced(const CObstacleInstance &obstacle) 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(shared_ptr CB); int3 repairScreenPos(int3 pos); //returns position closest to pos we can center screen on // show dialogs void showInfoDialog(const std::string &text, CComponent * component); void showInfoDialog(const std::string &text, const std::vector & components = std::vector(), int soundID = 0, bool delComps = false); void showInfoDialogAndWait(std::vector & components, const MetaString & text); void showOkDialog(std::vector & components, const MetaString & text, const std::function & onOk); void showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, bool DelComps = false, const std::vector & components = std::vector()); //deactivateCur - whether current main interface should be deactivated; delComps - if components will be deleted on window close 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(); void requestStoppingClient(); void sendCustomEvent(int code); void proposeLoadingGame(); ///returns true if all events are processed internally bool capturedAllEvents(); CPlayerInterface(PlayerColor Player);//c-tor ~CPlayerInterface();//d-tor static CondSh terminate_cond; // confirm termination 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; bool locked; void doMoveHero(const CGHeroInstance *h, CGPath path); }; extern CPlayerInterface * LOCPLINT; vcmi-0.98/client/CPreGame.cpp000066400000000000000000003654071250671757600160540ustar00rootroot00000000000000#include "StdInc.h" #include "CPreGame.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/CFileInfo.h" #include "../lib/filesystem/CCompressedStream.h" #include "../lib/CStopWatch.h" #include "gui/SDL_Extensions.h" #include "CGameInfo.h" #include "gui/CCursorHandler.h" #include "CDefHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CTownHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/mapping/CCampaignHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/JsonNode.h" #include "CMusicHandler.h" #include "CVideoHandler.h" #include "Graphics.h" #include "../lib/Connection.h" #include "../lib/VCMIDirs.h" #include "../lib/mapping/CMap.h" #include "windows/GUIClasses.h" #include "CPlayerInterface.h" #include "../CCallback.h" #include "CMessage.h" #include "../lib/spells/CSpellHandler.h" /*for campaign bonuses*/ #include "../lib/CArtHandler.h" /*for campaign bonuses*/ #include "../lib/CBuildingHandler.h" /*for campaign bonuses*/ #include "CBitmapHandler.h" #include "Client.h" #include "../lib/NetPacks.h" #include "../lib/registerTypes//RegisterTypes.h" #include "../lib/CThreadHelper.h" #include "../lib/CConfigHandler.h" #include "../lib/GameConstants.h" #include "gui/CGuiHandler.h" #include "gui/CAnimation.h" #include "widgets/CComponent.h" #include "widgets/Buttons.h" #include "widgets/MiscWidgets.h" #include "widgets/ObjectLists.h" #include "widgets/TextControls.h" #include "windows/InfoWindows.h" #include "../lib/mapping/CMapService.h" #include "../lib/mapping/CMap.h" #include "../lib/CRandomGenerator.h" /* * CPreGame.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ namespace fs = boost::filesystem; void startGame(StartInfo * options, CConnection *serv = nullptr); void endGame(); CGPreGame * CGP = nullptr; ISelectionScreenInfo *SEL; static PlayerColor playerColor; //if more than one player - applies to the first /** * Stores the current name of the savegame. * * TODO better solution for auto-selection when saving already saved games. * -> CSelectionScreen should be divided into CLoadGameScreen, CSaveGameScreen,... * The name of the savegame can then be stored non-statically in CGameState and * passed separately to CSaveGameScreen. */ static std::string saveGameName; struct EvilHlpStruct { CConnection *serv; StartInfo *sInfo; void reset(bool strong = true) { if(strong) { vstd::clear_pointer(serv); vstd::clear_pointer(sInfo); } else { serv = nullptr; sInfo = nullptr; } } } startingInfo; static void do_quit() { SDL_Event event; event.quit.type = SDL_QUIT; SDL_PushEvent(&event); } static CMapInfo *mapInfoFromGame() { auto ret = new CMapInfo(); ret->mapHeader = std::unique_ptr(new CMapHeader(*LOCPLINT->cb->getMapHeader())); return ret; } static void setPlayersFromGame() { playerColor = LOCPLINT->playerID; } static void swapPlayers(PlayerSettings &a, PlayerSettings &b) { std::swap(a.playerID, b.playerID); std::swap(a.name, b.name); if(a.playerID == 1) playerColor = a.color; else if(b.playerID == 1) playerColor = b.color; } void setPlayer(PlayerSettings &pset, ui8 player, const std::map &playerNames) { if(vstd::contains(playerNames, player)) pset.name = playerNames.find(player)->second; else pset.name = CGI->generaltexth->allTexts[468];//Computer pset.playerID = player; if(player == playerNames.begin()->first) playerColor = pset.color; } void updateStartInfo(std::string filename, StartInfo & sInfo, const CMapHeader * mapHeader, const std::map &playerNames) { sInfo.playerInfos.clear(); if(!mapHeader) { return; } sInfo.mapname = filename; playerColor = PlayerColor::NEUTRAL; auto namesIt = playerNames.cbegin(); for (int i = 0; i < mapHeader->players.size(); i++) { const PlayerInfo &pinfo = mapHeader->players[i]; //neither computer nor human can play - no player if (!(pinfo.canHumanPlay || pinfo.canComputerPlay)) continue; PlayerSettings &pset = sInfo.playerInfos[PlayerColor(i)]; pset.color = PlayerColor(i); if(pinfo.canHumanPlay && namesIt != playerNames.cend()) { setPlayer(pset, namesIt++->first, playerNames); } else { setPlayer(pset, 0, playerNames); if(!pinfo.canHumanPlay) { pset.compOnly = true; } } pset.castle = pinfo.defaultCastle(); pset.hero = pinfo.defaultHero(); if(pset.hero != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) { pset.hero = pinfo.mainCustomHeroId; pset.heroName = pinfo.mainCustomHeroName; pset.heroPortrait = pinfo.mainCustomHeroPortrait; } pset.handicap = PlayerSettings::NO_HANDICAP; } } template class CApplyOnPG; class CBaseForPGApply { public: virtual void applyOnPG(CSelectionScreen *selScr, void *pack) const =0; virtual ~CBaseForPGApply(){}; template static CBaseForPGApply *getApplier(const U * t=nullptr) { return new CApplyOnPG; } }; template class CApplyOnPG : public CBaseForPGApply { public: void applyOnPG(CSelectionScreen *selScr, void *pack) const { T *ptr = static_cast(pack); ptr->apply(selScr); } }; template <> class CApplyOnPG : public CBaseForPGApply { public: void applyOnPG(CSelectionScreen *selScr, void *pack) const { logGlobal->errorStream() << "Cannot apply on PG plain CPack!"; assert(0); } }; static CApplier *applier = nullptr; static CPicture* createPicture(const JsonNode& config) { return new CPicture(config["name"].String(), config["x"].Float(), config["y"].Float()); } CMenuScreen::CMenuScreen(const JsonNode& configNode): config(configNode) { OBJ_CONSTRUCTION_CAPTURING_ALL; background = new CPicture(config["background"].String()); if (config["scalable"].Bool()) { if (background->bg->format->palette) background->convertToScreenBPP(); background->scaleTo(Point(screen->w, screen->h)); } pos = background->center(); for(const JsonNode& node : config["items"].Vector()) menuNameToEntry.push_back(node["name"].String()); for(const JsonNode& node : config["images"].Vector()) images.push_back(createPicture(node)); //Hardcoded entry menuNameToEntry.push_back("credits"); tabs = new CTabbedInt(std::bind(&CMenuScreen::createTab, this, _1), CTabbedInt::DestroyFunc()); tabs->type |= REDRAW_PARENT; } CIntObject * CMenuScreen::createTab(size_t index) { if (config["items"].Vector().size() == index) return new CreditsScreen(); return new CMenuEntry(this, config["items"].Vector()[index]); } void CMenuScreen::showAll(SDL_Surface * to) { CIntObject::showAll(to); if (pos.h != to->h || pos.w != to->w) CMessage::drawBorder(PlayerColor(1), to, pos.w+28, pos.h+30, pos.x-14, pos.y-15); } void CMenuScreen::show(SDL_Surface * to) { if (!config["video"].isNull()) CCS->videoh->update(config["video"]["x"].Float() + pos.x, config["video"]["y"].Float() + pos.y, to, true, false); CIntObject::show(to); } void CMenuScreen::activate() { CCS->musich->playMusic("Music/MainMenu", true); if (!config["video"].isNull()) CCS->videoh->open(config["video"]["name"].String()); CIntObject::activate(); } void CMenuScreen::deactivate() { if (!config["video"].isNull()) CCS->videoh->close(); CIntObject::deactivate(); } void CMenuScreen::switchToTab(size_t index) { tabs->setActive(index); } //funciton for std::string -> std::function conversion for main menu static std::function genCommand(CMenuScreen* menu, std::vector menuType, const std::string &string) { static const std::vector commandType = {"to", "campaigns", "start", "load", "exit", "highscores"}; static const std::vector gameType = {"single", "multi", "campaign", "tutorial"}; std::list commands; boost::split(commands, string, boost::is_any_of("\t ")); if (!commands.empty()) { size_t index = std::find(commandType.begin(), commandType.end(), commands.front()) - commandType.begin(); commands.pop_front(); if (index > 3 || !commands.empty()) { switch (index) { break; case 0://to - switch to another tab, if such tab exists { size_t index2 = std::find(menuType.begin(), menuType.end(), commands.front()) - menuType.begin(); if ( index2 != menuType.size()) return std::bind(&CMenuScreen::switchToTab, menu, index2); } break; case 1://open campaign selection window { return std::bind(&CGPreGame::openCampaignScreen, CGP, commands.front()); } break; case 2://start { switch (std::find(gameType.begin(), gameType.end(), commands.front()) - gameType.begin()) { case 0: return std::bind(&CGPreGame::openSel, CGP, CMenuScreen::newGame, CMenuScreen::SINGLE_PLAYER); case 1: return &pushIntT; case 2: return std::bind(&CGPreGame::openSel, CGP, CMenuScreen::campaignList, CMenuScreen::SINGLE_PLAYER); //TODO: start tutorial case 3: return std::bind(CInfoWindow::showInfoDialog, "Sorry, tutorial is not implemented yet\n", (const std::vector*)nullptr, false, PlayerColor(1)); } } break; case 3://load { switch (std::find(gameType.begin(), gameType.end(), commands.front()) - gameType.begin()) { case 0: return std::bind(&CGPreGame::openSel, CGP, CMenuScreen::loadGame, CMenuScreen::SINGLE_PLAYER); case 1: return std::bind(&CGPreGame::openSel, CGP, CMenuScreen::loadGame, CMenuScreen::MULTI_HOT_SEAT); //TODO: load campaign case 2: return std::bind(CInfoWindow::showInfoDialog, "This function is not implemented yet. Campaign saves can be loaded from \"Single Player\" menu", (const std::vector*)nullptr, false, PlayerColor(1)); //TODO: load tutorial case 3: return std::bind(CInfoWindow::showInfoDialog, "Sorry, tutorial is not implemented yet\n", (const std::vector*)nullptr, false, PlayerColor(1)); } } break; case 4://exit { return std::bind(CInfoWindow::showYesNoDialog, std::ref(CGI->generaltexth->allTexts[69]), (const std::vector*)nullptr, do_quit, 0, false, PlayerColor(1)); } break; case 5://highscores { //TODO: high scores return std::bind(CInfoWindow::showInfoDialog, "Sorry, high scores menu is not implemented yet\n", (const std::vector*)nullptr, false, PlayerColor(1)); } } } } logGlobal->errorStream()<<"Failed to parse command: "<(); } CButton* CMenuEntry::createButton(CMenuScreen* parent, const JsonNode& button) { std::function command = genCommand(parent, parent->menuNameToEntry, button["command"].String()); std::pair help; if (!button["help"].isNull() && button["help"].Float() > 0) help = CGI->generaltexth->zelp[button["help"].Float()]; int posx = button["x"].Float(); if (posx < 0) posx = pos.w + posx; int posy = button["y"].Float(); if (posy < 0) posy = pos.h + posy; return new CButton(Point(posx, posy), button["name"].String(), help, command, button["hotkey"].Float()); } CMenuEntry::CMenuEntry(CMenuScreen* parent, const JsonNode &config) { OBJ_CONSTRUCTION_CAPTURING_ALL; type |= REDRAW_PARENT; pos = parent->pos; for(const JsonNode& node : config["images"].Vector()) images.push_back(createPicture(node)); for(const JsonNode& node : config["buttons"].Vector()) { buttons.push_back(createButton(parent, node)); buttons.back()->hoverable = true; buttons.back()->type |= REDRAW_PARENT; } } CreditsScreen::CreditsScreen(): positionCounter(0) { addUsedEvents(LCLICK | RCLICK); type |= REDRAW_PARENT; OBJ_CONSTRUCTION_CAPTURING_ALL; pos.w = CGP->menu->pos.w; pos.h = CGP->menu->pos.h; auto textFile = CResourceHandler::get()->load(ResourceID("DATA/CREDITS.TXT"))->readAll(); std::string text((char*)textFile.first.get(), textFile.second); size_t firstQuote = text.find('\"')+1; text = text.substr(firstQuote, text.find('\"', firstQuote) - firstQuote ); credits = new CMultiLineLabel(Rect(pos.w - 350, 0, 350, 600), FONT_CREDITS, CENTER, Colors::WHITE, text); credits->scrollTextTo(-600); // move all text below the screen } void CreditsScreen::show(SDL_Surface * to) { CIntObject::show(to); positionCounter++; if (positionCounter % 2 == 0) credits->scrollTextBy(1); //end of credits, close this screen if (credits->textSize.y + 600 < positionCounter / 2) clickRight(false, false); } void CreditsScreen::clickLeft(tribool down, bool previousState) { clickRight(down, previousState); } void CreditsScreen::clickRight(tribool down, bool previousState) { CTabbedInt* menu = dynamic_cast(parent); assert(menu); menu->setActive(0); } CGPreGameConfig & CGPreGameConfig::get() { static CGPreGameConfig config; return config; } const JsonNode & CGPreGameConfig::getConfig() const { return config; } const JsonNode & CGPreGameConfig::getCampaigns() const { return campaignSets; } CGPreGameConfig::CGPreGameConfig() : campaignSets(JsonNode(ResourceID("config/campaignSets.json"))), config(JsonNode(ResourceID("config/mainmenu.json"))) { } CGPreGame::CGPreGame() { pos.w = screen->w; pos.h = screen->h; GH.defActionsDef = 63; CGP = this; menu = new CMenuScreen(CGPreGameConfig::get().getConfig()["window"]); loadGraphics(); } CGPreGame::~CGPreGame() { boost::unique_lock lock(*CPlayerInterface::pim); disposeGraphics(); if(CGP == this) CGP = nullptr; if(GH.curInt == this) GH.curInt = nullptr; } void CGPreGame::openSel(CMenuScreen::EState screenType, CMenuScreen::EMultiMode multi /*= CMenuScreen::SINGLE_PLAYER*/) { GH.pushInt(new CSelectionScreen(screenType, multi)); } void CGPreGame::loadGraphics() { OBJ_CONSTRUCTION_CAPTURING_ALL; new CFilledTexture("DIBOXBCK", pos); victory = CDefHandler::giveDef("SCNRVICT.DEF"); loss = CDefHandler::giveDef("SCNRLOSS.DEF"); } void CGPreGame::disposeGraphics() { delete victory; delete loss; } void CGPreGame::update() { boost::unique_lock lock(*CPlayerInterface::pim); if(CGP != this) //don't update if you are not a main interface return; if (GH.listInt.empty()) { GH.pushInt(this); GH.pushInt(menu); menu->switchToTab(0); } if(SEL) SEL->update(); // Handles mouse and key input GH.updateTime(); GH.handleEvents(); // check for null othervice crash on finishing a campaign // /FIXME: find out why GH.listInt is empty to begin with if (GH.topInt() != nullptr) GH.topInt()->show(screen); if (settings["general"]["showfps"].Bool()) GH.drawFPSCounter(); } void CGPreGame::runLocked(std::function cb) { boost::unique_lock lock(*CPlayerInterface::pim); cb(); } void CGPreGame::openCampaignScreen(std::string name) { if (vstd::contains(CGPreGameConfig::get().getCampaigns().Struct(), name)) { GH.pushInt(new CCampaignScreen(CGPreGameConfig::get().getCampaigns()[name])); return; } logGlobal->errorStream()<<"Unknown campaign set: "< * Names /*= nullptr*/, const std::string & Address /*=""*/, const std::string & Port /*= ""*/) : ISelectionScreenInfo(Names), serverHandlingThread(nullptr), mx(new boost::recursive_mutex), serv(nullptr), ongoingClosing(false), myNameID(255) { CGPreGame::create(); //we depend on its graphics screenType = Type; multiPlayer = MultiPlayer; OBJ_CONSTRUCTION_CAPTURING_ALL; bool network = (isGuest() || isHost()); CServerHandler *sh = nullptr; if(isHost()) { sh = new CServerHandler; sh->startServer(); } IShowActivatable::type = BLOCK_ADV_HOTKEYS; pos.w = 762; pos.h = 584; if(Type == CMenuScreen::saveGame) { bordered = false; center(pos); } else if(Type == CMenuScreen::campaignList) { bordered = false; bg = new CPicture("CamCust.bmp", 0, 0); pos = bg->center(); } else { bordered = true; //load random background const JsonVector & bgNames = CGPreGameConfig::get().getConfig()["game-select"].Vector(); bg = new CPicture(RandomGeneratorUtil::nextItem(bgNames, CRandomGenerator::getDefault())->String(), 0, 0); pos = bg->center(); } sInfo.difficulty = 1; current = nullptr; sInfo.mode = (Type == CMenuScreen::newGame ? StartInfo::NEW_GAME : StartInfo::LOAD_GAME); sInfo.turnTime = 0; curTab = nullptr; card = new InfoCard(network); //right info card if (screenType == CMenuScreen::campaignList) { opt = nullptr; } else { opt = new OptionsTab(); //scenario options tab opt->recActions = DISPOSE; randMapTab = new CRandomMapTab(); randMapTab->getMapInfoChanged() += std::bind(&CSelectionScreen::changeSelection, this, _1); randMapTab->recActions = DISPOSE; } sel = new SelectionTab(screenType, std::bind(&CSelectionScreen::changeSelection, this, _1), multiPlayer); //scenario selection tab sel->recActions = DISPOSE; switch(screenType) { case CMenuScreen::newGame: { SDL_Color orange = {232, 184, 32, 0}; SDL_Color overlayColor = isGuest() ? orange : Colors::WHITE; card->difficulty->addCallback(std::bind(&CSelectionScreen::difficultyChange, this, _1)); card->difficulty->setSelected(1); CButton * select = new CButton(Point(411, 80), "GSPBUTT.DEF", CGI->generaltexth->zelp[45], 0, SDLK_s); select->addCallback([&]() { toggleTab(sel); changeSelection(sel->getSelectedMapInfo()); }); select->addTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, overlayColor); CButton *opts = new CButton(Point(411, 510), "GSPBUTT.DEF", CGI->generaltexth->zelp[46], std::bind(&CSelectionScreen::toggleTab, this, opt), SDLK_a); opts->addTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, overlayColor); CButton * randomBtn = new CButton(Point(411, 105), "GSPBUTT.DEF", CGI->generaltexth->zelp[47], 0, SDLK_r); randomBtn->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, overlayColor); randomBtn->addCallback([&]() { toggleTab(randMapTab); changeSelection(randMapTab->getMapInfo()); }); start = new CButton(Point(411, 535), "SCNRBEG.DEF", CGI->generaltexth->zelp[103], std::bind(&CSelectionScreen::startScenario, this), SDLK_b); if(network) { CButton *hideChat = new CButton(Point(619, 83), "GSPBUT2.DEF", CGI->generaltexth->zelp[48], std::bind(&InfoCard::toggleChat, card), SDLK_h); hideChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL); if(isGuest()) { select->block(true); opts->block(true); randomBtn->block(true); start->block(true); } } } break; case CMenuScreen::loadGame: sel->recActions = 255; start = new CButton(Point(411, 535), "SCNRLOD.DEF", CGI->generaltexth->zelp[103], std::bind(&CSelectionScreen::startScenario, this), SDLK_l); break; case CMenuScreen::saveGame: sel->recActions = 255; start = new CButton(Point(411, 535), "SCNRSAV.DEF", CGI->generaltexth->zelp[103], std::bind(&CSelectionScreen::startScenario, this), SDLK_s); break; case CMenuScreen::campaignList: sel->recActions = 255; start = new CButton(Point(411, 535), "SCNRLOD.DEF", CButton::tooltip(), std::bind(&CSelectionScreen::startCampaign, this), SDLK_b); break; } start->assignedKeys.insert(SDLK_RETURN); back = new CButton(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], std::bind(&CGuiHandler::popIntTotally, &GH, this), SDLK_ESCAPE); if(network) { if(isHost()) { assert(playerNames.size() == 1 && vstd::contains(playerNames, 1)); //TODO hot-seat/network combo serv = sh->connectToServer(); *serv << (ui8) 4; myNameID = 1; } else serv = CServerHandler::justConnectToServer(Address, Port); serv->enterPregameConnectionMode(); *serv << playerNames.begin()->second; if(isGuest()) { const CMapInfo *map; *serv >> myNameID >> map; serv->connectionID = myNameID; changeSelection(map); } else if(current) { SelectMap sm(*current); *serv << &sm; UpdateStartOptions uso(sInfo); *serv << &uso; } applier = new CApplier; registerTypesPregamePacks(*applier); serverHandlingThread = new boost::thread(&CSelectionScreen::handleConnection, this); } delete sh; } CSelectionScreen::~CSelectionScreen() { ongoingClosing = true; if(serv) { assert(serverHandlingThread); QuitMenuWithoutStarting qmws; *serv << &qmws; // while(!serverHandlingThread->timed_join(boost::posix_time::milliseconds(50))) // processPacks(); serverHandlingThread->join(); delete serverHandlingThread; } playerColor = PlayerColor::CANNOT_DETERMINE; playerNames.clear(); assert(!serv); vstd::clear_pointer(applier); delete mx; } void CSelectionScreen::toggleTab(CIntObject *tab) { if(isHost() && serv) { PregameGuiAction pga; if(tab == curTab) pga.action = PregameGuiAction::NO_TAB; else if(tab == opt) pga.action = PregameGuiAction::OPEN_OPTIONS; else if(tab == sel) pga.action = PregameGuiAction::OPEN_SCENARIO_LIST; else if(tab == randMapTab) pga.action = PregameGuiAction::OPEN_RANDOM_MAP_OPTIONS; *serv << &pga; } if(curTab && curTab->active) { curTab->deactivate(); curTab->recActions = DISPOSE; } if(curTab != tab) { tab->recActions = 255; tab->activate(); curTab = tab; } else { curTab = nullptr; }; GH.totalRedraw(); } void CSelectionScreen::changeSelection(const CMapInfo * to) { if(current == to) return; if(isGuest()) vstd::clear_pointer(current); current = to; if(to && (screenType == CMenuScreen::loadGame || screenType == CMenuScreen::saveGame)) SEL->sInfo.difficulty = to->scenarioOpts->difficulty; if(screenType != CMenuScreen::campaignList) { updateStartInfo(to ? to->fileURI : "", sInfo, to ? to->mapHeader.get() : nullptr); if(screenType == CMenuScreen::newGame) { if(to && to->isRandomMap) { sInfo.mapGenOptions = std::shared_ptr(new CMapGenOptions(randMapTab->getMapGenOptions())); } else { sInfo.mapGenOptions.reset(); } } } card->changeSelection(to); if(screenType != CMenuScreen::campaignList) { opt->recreate(); } if(isHost() && serv) { SelectMap sm(*to); *serv << &sm; UpdateStartOptions uso(sInfo); *serv << &uso; } } void CSelectionScreen::startCampaign() { if (SEL->current) GH.pushInt(new CBonusSelection(SEL->current->fileURI)); } void CSelectionScreen::startScenario() { if(screenType == CMenuScreen::newGame) { //there must be at least one human player before game can be started std::map::const_iterator i; for(i = SEL->sInfo.playerInfos.cbegin(); i != SEL->sInfo.playerInfos.cend(); i++) if(i->second.playerID != PlayerSettings::PLAYER_AI) break; if(i == SEL->sInfo.playerInfos.cend()) { GH.pushInt(CInfoWindow::create(CGI->generaltexth->allTexts[530])); //You must position yourself prior to starting the game. return; } } if(isHost()) { start->block(true); StartWithCurrentSettings swcs; *serv << &swcs; ongoingClosing = true; return; } if(screenType != CMenuScreen::saveGame) { if(!current) return; if(sInfo.mapGenOptions) { //copy settings from interface to actual options. TODO: refactor, it used to have no effect at all -.- sInfo.mapGenOptions = std::shared_ptr(new CMapGenOptions(randMapTab->getMapGenOptions())); // Update player settings for RMG for(const auto & psetPair : sInfo.playerInfos) { const auto & pset = psetPair.second; sInfo.mapGenOptions->setStartingTownForPlayer(pset.color, pset.castle); if(pset.playerID != PlayerSettings::PLAYER_AI) { sInfo.mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::HUMAN); } } if(!sInfo.mapGenOptions->checkOptions()) { GH.pushInt(CInfoWindow::create(CGI->generaltexth->allTexts[751])); return; } } saveGameName.clear(); if(screenType == CMenuScreen::loadGame) { saveGameName = sInfo.mapname; } auto si = new StartInfo(sInfo); CGP->removeFromGui(); CGP->showLoadingScreen(std::bind(&startGame, si, (CConnection *)nullptr)); } else { if(!(sel && sel->txt && sel->txt->text.size())) return; saveGameName = "Saves/" + sel->txt->text; CFunctionList overWrite; overWrite += std::bind(&CCallback::save, LOCPLINT->cb.get(), saveGameName); overWrite += std::bind(&CGuiHandler::popIntTotally, &GH, this); if(CResourceHandler::get("local")->existsResource(ResourceID(saveGameName, EResType::CLIENT_SAVEGAME))) { std::string hlp = CGI->generaltexth->allTexts[493]; //%s exists. Overwrite? boost::algorithm::replace_first(hlp, "%s", sel->txt->text); LOCPLINT->showYesNoDialog(hlp, overWrite, 0, false); } else overWrite(); } } void CSelectionScreen::difficultyChange( int to ) { assert(screenType == CMenuScreen::newGame); sInfo.difficulty = to; propagateOptions(); redraw(); } void CSelectionScreen::handleConnection() { setThreadName("CSelectionScreen::handleConnection"); try { assert(serv); while(serv) { CPackForSelectionScreen *pack = nullptr; *serv >> pack; logNetwork->traceStream() << "Received a pack of type " << typeid(*pack).name(); assert(pack); if(QuitMenuWithoutStarting *endingPack = dynamic_cast(pack)) { endingPack->apply(this); } else if(StartWithCurrentSettings *endingPack = dynamic_cast(pack)) { endingPack->apply(this); } else { boost::unique_lock lll(*mx); upcomingPacks.push_back(pack); } } } catch(int i) { if(i != 666) throw; } catch(...) { handleException(); throw; } } void CSelectionScreen::setSInfo(const StartInfo &si) { std::map::const_iterator i; for(i = si.playerInfos.cbegin(); i != si.playerInfos.cend(); i++) { if(i->second.playerID == myNameID) { playerColor = i->first; break; } } if(i == si.playerInfos.cend()) //not found playerColor = PlayerColor::CANNOT_DETERMINE; sInfo = si; if(current) opt->recreate(); //will force to recreate using current sInfo card->difficulty->setSelected(si.difficulty); if(curTab == randMapTab) randMapTab->setMapGenOptions(si.mapGenOptions); GH.totalRedraw(); } void CSelectionScreen::processPacks() { boost::unique_lock lll(*mx); while(!upcomingPacks.empty()) { CPackForSelectionScreen *pack = upcomingPacks.front(); upcomingPacks.pop_front(); CBaseForPGApply *apply = applier->apps[typeList.getTypeID(pack)]; //find the applier apply->applyOnPG(this, pack); delete pack; } } void CSelectionScreen::update() { if(serverHandlingThread) processPacks(); } void CSelectionScreen::propagateOptions() { if(isHost() && serv) { UpdateStartOptions ups(sInfo); *serv << &ups; } } void CSelectionScreen::postRequest(ui8 what, ui8 dir) { if(!isGuest() || !serv) return; RequestOptionsChange roc(what, dir, myNameID); *serv << &roc; } void CSelectionScreen::postChatMessage(const std::string &txt) { assert(serv); ChatMessage cm; cm.message = txt; cm.playerName = sInfo.getPlayersSettings(myNameID)->name; *serv << &cm; } void CSelectionScreen::propagateNames() { PlayersNames pn; pn.playerNames = playerNames; *serv << &pn; } void CSelectionScreen::showAll(SDL_Surface *to) { CIntObject::showAll(to); if (bordered && (pos.h != to->h || pos.w != to->w)) CMessage::drawBorder(PlayerColor(1), to, pos.w+28, pos.h+30, pos.x-14, pos.y-15); } // A new size filter (Small, Medium, ...) has been selected. Populate // selMaps with the relevant data. void SelectionTab::filter( int size, bool selectFirst ) { curItems.clear(); if(tabType == CMenuScreen::campaignList) { for (auto & elem : allItems) curItems.push_back(&elem); } else { for (auto & elem : allItems) if( elem.mapHeader && elem.mapHeader->version && (!size || elem.mapHeader->width == size)) curItems.push_back(&elem); } if(curItems.size()) { slider->block(false); slider->setAmount(curItems.size()); sort(); if(selectFirst) { slider->moveTo(0); onSelect(curItems[0]); } selectAbs(0); } else { slider->block(true); onSelect(nullptr); } } std::unordered_set SelectionTab::getFiles(std::string dirURI, int resType) { boost::to_upper(dirURI); std::unordered_set ret = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) { return ident.getType() == resType && boost::algorithm::starts_with(ident.getName(), dirURI); }); return ret; } void SelectionTab::parseMaps(const std::unordered_set &files) { allItems.clear(); for(auto & file : files) { try { CMapInfo mapInfo; mapInfo.mapInit(file.getName()); // ignore unsupported map versions (e.g. WoG maps without WoG if (mapInfo.mapHeader->version <= CGI->modh->settings.data["textData"]["mapVersion"].Float()) allItems.push_back(std::move(mapInfo)); } catch(std::exception & e) { logGlobal->errorStream() << "Map " << file.getName() << " is invalid. Message: " << e.what(); } } } void SelectionTab::parseGames(const std::unordered_set &files, bool multi) { for(auto & file : files) { try { CLoadFile lf(*CResourceHandler::get()->getResourceName(file), minSupportedVersion); lf.checkMagicBytes(SAVEGAME_MAGIC); // ui8 sign[8]; // lf >> sign; // if(std::memcmp(sign,"VCMISVG",7)) // { // throw std::runtime_error("not a correct savefile!"); // } // Create the map info object CMapInfo mapInfo; mapInfo.mapHeader = make_unique(); mapInfo.scenarioOpts = new StartInfo; lf >> *(mapInfo.mapHeader.get()) >> mapInfo.scenarioOpts; mapInfo.fileURI = file.getName(); mapInfo.countPlayers(); std::time_t time = CFileInfo(*CResourceHandler::get()->getResourceName(file)).getDate(); mapInfo.date = std::asctime(std::localtime(&time)); // If multi mode then only multi games, otherwise single if((mapInfo.actualHumanPlayers > 1) != multi) { mapInfo.mapHeader.reset(); } allItems.push_back(std::move(mapInfo)); } catch(const std::exception & e) { logGlobal->errorStream() << "Error: Failed to process " << file.getName() <<": " << e.what(); } } } void SelectionTab::parseCampaigns(const std::unordered_set &files ) { allItems.reserve(files.size()); for (auto & file : files) { CMapInfo info; //allItems[i].date = std::asctime(std::localtime(&files[i].date)); info.fileURI = file.getName(); info.campaignInit(); allItems.push_back(std::move(info)); } } SelectionTab::SelectionTab(CMenuScreen::EState Type, const std::function &OnSelect, CMenuScreen::EMultiMode MultiPlayer /*= CMenuScreen::SINGLE_PLAYER*/) :bg(nullptr), onSelect(OnSelect) { OBJ_CONSTRUCTION; selectionPos = 0; addUsedEvents(LCLICK | WHEEL | KEYBOARD | DOUBLECLICK); slider = nullptr; txt = nullptr; tabType = Type; if (Type != CMenuScreen::campaignList) { bg = new CPicture("SCSELBCK.bmp", 0, 6); pos = bg->pos; } else { bg = nullptr; //use background from parent type |= REDRAW_PARENT; // we use parent background so we need to make sure it's will be redrawn too pos.w = parent->pos.w; pos.h = parent->pos.h; pos.x += 3; pos.y += 6; } if(MultiPlayer == CMenuScreen::MULTI_NETWORK_GUEST) { positions = 18; } else { switch(tabType) { case CMenuScreen::newGame: parseMaps(getFiles("Maps/", EResType::MAP)); positions = 18; break; case CMenuScreen::loadGame: case CMenuScreen::saveGame: parseGames(getFiles("Saves/", EResType::CLIENT_SAVEGAME), MultiPlayer); if(tabType == CMenuScreen::loadGame) { positions = 18; } else { positions = 16; } if(tabType == CMenuScreen::saveGame) { txt = new CTextInput(Rect(32, 539, 350, 20), Point(-32, -25), "GSSTRIP.bmp", 0); txt->filters += CTextInput::filenameFilter; } break; case CMenuScreen::campaignList: parseCampaigns(getFiles("Maps/", EResType::CAMPAIGN)); positions = 18; break; default: assert(0); break; } } generalSortingBy = (tabType == CMenuScreen::loadGame || tabType == CMenuScreen::saveGame) ? _fileName : _name; if (tabType != CMenuScreen::campaignList) { //size filter buttons { int sizes[] = {36, 72, 108, 144, 0}; const char * names[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"}; for(int i = 0; i < 5; i++) new CButton(Point(158 + 47*i, 46), names[i], CGI->generaltexth->zelp[54+i], std::bind(&SelectionTab::filter, this, sizes[i], true)); } //sort buttons buttons { int xpos[] = {23, 55, 88, 121, 306, 339}; const char * names[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"}; for(int i = 0; i < 6; i++) { ESortBy criteria = (ESortBy)i; if(criteria == _name) criteria = generalSortingBy; new CButton(Point(xpos[i], 86), names[i], CGI->generaltexth->zelp[107+i], std::bind(&SelectionTab::sortBy, this, criteria)); } } } else { //sort by buttons new CButton(Point(23, 86), "CamCusM.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps)); //by num of maps new CButton(Point(55, 86), "CamCusL.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name)); //by name } slider = new CSlider(Point(372, 86), tabType != CMenuScreen::saveGame ? 480 : 430, std::bind(&SelectionTab::sliderMove, this, _1), positions, curItems.size(), 0, false, CSlider::BLUE); slider->addUsedEvents(WHEEL); format = CDefHandler::giveDef("SCSELC.DEF"); sortingBy = _format; ascending = true; filter(0); //select(0); switch(tabType) { case CMenuScreen::newGame: selectFName("Maps/Arrogance"); break; case CMenuScreen::loadGame: case CMenuScreen::campaignList: select(0); break; case CMenuScreen::saveGame:; if(saveGameName.empty()) { txt->setText("NEWGAME"); } else { selectFName(saveGameName); } } } SelectionTab::~SelectionTab() { delete format; } void SelectionTab::sortBy( int criteria ) { if(criteria == sortingBy) { ascending = !ascending; } else { sortingBy = (ESortBy)criteria; ascending = true; } sort(); selectAbs(0); } void SelectionTab::sort() { if(sortingBy != generalSortingBy) std::stable_sort(curItems.begin(), curItems.end(), mapSorter(generalSortingBy)); std::stable_sort(curItems.begin(), curItems.end(), mapSorter(sortingBy)); if(!ascending) std::reverse(curItems.begin(), curItems.end()); redraw(); } void SelectionTab::select( int position ) { if(!curItems.size()) return; // New selection. py is the index in curItems. int py = position + slider->getValue(); vstd::amax(py, 0); vstd::amin(py, curItems.size()-1); selectionPos = py; if(position < 0) slider->moveBy(position); else if(position >= positions) slider->moveBy(position - positions + 1); if(txt) { std::string filename = *CResourceHandler::get("local")->getResourceName( ResourceID(curItems[py]->fileURI, EResType::CLIENT_SAVEGAME)); txt->setText(CFileInfo(filename).getBaseName()); } onSelect(curItems[py]); } void SelectionTab::selectAbs( int position ) { select(position - slider->getValue()); } int SelectionTab::getPosition( int x, int y ) { return -1; } void SelectionTab::sliderMove( int slidPos ) { if(!slider) return; //ignore spurious call when slider is being created redraw(); } // Display the tab with the scenario names // // elemIdx is the index of the maps or saved game to display on line 0 // slider->capacity contains the number of available screen lines // slider->positionsAmnt is the number of elements after filtering void SelectionTab::printMaps(SDL_Surface *to) { int elemIdx = slider->getValue(); // Display all elements if there's enough space //if(slider->amount < slider->capacity) // elemIdx = 0; SDL_Color itemColor; for (int line = 0; line < positions && elemIdx < curItems.size(); elemIdx++, line++) { CMapInfo *currentItem = curItems[elemIdx]; if (elemIdx == selectionPos) itemColor=Colors::YELLOW; else itemColor=Colors::WHITE; if(tabType != CMenuScreen::campaignList) { //amount of players std::ostringstream ostr(std::ostringstream::out); ostr << currentItem->playerAmnt << "/" << currentItem->humanPlayers; printAtLoc(ostr.str(), 29, 120 + line * 25, FONT_SMALL, itemColor, to); //map size std::string temp2 = "C"; switch (currentItem->mapHeader->width) { case 36: temp2="S"; break; case 72: temp2="M"; break; case 108: temp2="L"; break; case 144: temp2="XL"; break; } printAtMiddleLoc(temp2, 70, 128 + line * 25, FONT_SMALL, itemColor, to); int temp=-1; switch (currentItem->mapHeader->version) { case EMapFormat::ROE: temp=0; break; case EMapFormat::AB: temp=1; break; case EMapFormat::SOD: temp=2; break; case EMapFormat::WOG: temp=3; break; default: // Unknown version. Be safe and ignore that map logGlobal->warnStream() << "Warning: " << currentItem->fileURI << " has wrong version!"; continue; } blitAtLoc(format->ourImages[temp].bitmap, 88, 117 + line * 25, to); //victory conditions blitAtLoc(CGP->victory->ourImages[currentItem->mapHeader->victoryIconIndex].bitmap, 306, 117 + line * 25, to); //loss conditions blitAtLoc(CGP->loss->ourImages[currentItem->mapHeader->defeatIconIndex].bitmap, 339, 117 + line * 25, to); } else //if campaign { //number of maps in campaign std::ostringstream ostr(std::ostringstream::out); ostr << CGI->generaltexth->campaignRegionNames[ currentItem->campaignHeader->mapVersion ].size(); printAtLoc(ostr.str(), 29, 120 + line * 25, FONT_SMALL, itemColor, to); } std::string name; if(tabType == CMenuScreen::newGame) { if (!currentItem->mapHeader->name.length()) currentItem->mapHeader->name = "Unnamed"; name = currentItem->mapHeader->name; } else if(tabType == CMenuScreen::campaignList) { name = currentItem->campaignHeader->name; } else { name = CFileInfo(*CResourceHandler::get("local")->getResourceName( ResourceID(currentItem->fileURI, EResType::CLIENT_SAVEGAME))).getBaseName(); } //print name printAtMiddleLoc(name, 213, 128 + line * 25, FONT_SMALL, itemColor, to); } } void SelectionTab::showAll(SDL_Surface * to) { CIntObject::showAll(to); printMaps(to); std::string title; switch(tabType) { case CMenuScreen::newGame: title = CGI->generaltexth->arraytxt[229]; break; case CMenuScreen::loadGame: title = CGI->generaltexth->arraytxt[230]; break; case CMenuScreen::saveGame: title = CGI->generaltexth->arraytxt[231]; break; case CMenuScreen::campaignList: title = CGI->generaltexth->allTexts[726]; break; } printAtMiddleLoc(title, 205, 28, FONT_MEDIUM, Colors::YELLOW, to); //Select a Scenario to Play if(tabType != CMenuScreen::campaignList) { printAtMiddleLoc(CGI->generaltexth->allTexts[510], 87, 62, FONT_SMALL, Colors::YELLOW, to); //Map sizes } } void SelectionTab::clickLeft( tribool down, bool previousState ) { if(down) { int line = getLine(); if(line != -1) select(line); } } void SelectionTab::keyPressed( const SDL_KeyboardEvent & key ) { if(key.state != SDL_PRESSED) return; int moveBy = 0; switch(key.keysym.sym) { case SDLK_UP: moveBy = -1; break; case SDLK_DOWN: moveBy = +1; break; case SDLK_PAGEUP: moveBy = -positions+1; break; case SDLK_PAGEDOWN: moveBy = +positions-1; break; case SDLK_HOME: select(-slider->getValue()); return; case SDLK_END: select(curItems.size() - slider->getValue()); return; default: return; } select(selectionPos - slider->getValue() + moveBy); } void SelectionTab::onDoubleClick() { if(getLine() != -1) //double clicked scenarios list { //act as if start button was pressed (static_cast(parent))->start->clickLeft(false, true); } } int SelectionTab::getLine() { int line = -1; Point clickPos(GH.current->button.x, GH.current->button.y); clickPos = clickPos - pos.topLeft(); // Ignore clicks on save name area int maxPosY; if(tabType == CMenuScreen::saveGame) maxPosY = 516; else maxPosY = 564; if(clickPos.y > 115 && clickPos.y < maxPosY && clickPos.x > 22 && clickPos.x < 371) { line = (clickPos.y-115) / 25; //which line } return line; } void SelectionTab::selectFName( std::string fname ) { boost::to_upper(fname); for(int i = curItems.size() - 1; i >= 0; i--) { if(curItems[i]->fileURI == fname) { slider->moveTo(i); selectAbs(i); return; } } selectAbs(0); } const CMapInfo * SelectionTab::getSelectedMapInfo() const { return curItems.empty() ? nullptr : curItems[selectionPos]; } CRandomMapTab::CRandomMapTab() { OBJ_CONSTRUCTION; bg = new CPicture("RANMAPBK", 0, 6); // Map Size mapSizeBtnGroup = new CToggleGroup(0); mapSizeBtnGroup->pos.y += 81; mapSizeBtnGroup->pos.x += 158; const std::vector mapSizeBtns = {"RANSIZS", "RANSIZM","RANSIZL","RANSIZX"}; addButtonsToGroup(mapSizeBtnGroup, mapSizeBtns, 0, 3, 47, 198); mapSizeBtnGroup->setSelected(1); mapSizeBtnGroup->addCallback([&](int btnId) { auto mapSizeVal = getPossibleMapSizes(); mapGenOptions.setWidth(mapSizeVal[btnId]); mapGenOptions.setHeight(mapSizeVal[btnId]); if(!SEL->isGuest()) updateMapInfo(); }); // Two levels twoLevelsBtn = new CToggleButton(Point(346, 81), "RANUNDR", CGI->generaltexth->zelp[202]); //twoLevelsBtn->select(true); for now, deactivated twoLevelsBtn->addCallback([&](bool on) { mapGenOptions.setHasTwoLevels(on); if(!SEL->isGuest()) updateMapInfo(); }); // Create number defs list std::vector numberDefs; for(int i = 0; i <= 8; ++i) { numberDefs.push_back("RANNUM" + boost::lexical_cast(i)); } const int NUMBERS_WIDTH = 32; const int BTNS_GROUP_LEFT_MARGIN = 67; // Amount of players playersCntGroup = new CToggleGroup(0); playersCntGroup->pos.y += 153; playersCntGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; addButtonsWithRandToGroup(playersCntGroup, numberDefs, 1, 8, NUMBERS_WIDTH, 204, 212); playersCntGroup->addCallback([&](int btnId) { mapGenOptions.setPlayerCount(btnId); deactivateButtonsFrom(teamsCntGroup, btnId); deactivateButtonsFrom(compOnlyPlayersCntGroup, 8 - btnId + 1); validatePlayersCnt(btnId); if(!SEL->isGuest()) updateMapInfo(); }); // Amount of teams teamsCntGroup = new CToggleGroup(0); teamsCntGroup->pos.y += 219; teamsCntGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; addButtonsWithRandToGroup(teamsCntGroup, numberDefs, 0, 7, NUMBERS_WIDTH, 214, 222); teamsCntGroup->addCallback([&](int btnId) { mapGenOptions.setTeamCount(btnId); if(!SEL->isGuest()) updateMapInfo(); }); // Computer only players compOnlyPlayersCntGroup = new CToggleGroup(0); compOnlyPlayersCntGroup->pos.y += 285; compOnlyPlayersCntGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; addButtonsWithRandToGroup(compOnlyPlayersCntGroup, numberDefs, 0, 7, NUMBERS_WIDTH, 224, 232); compOnlyPlayersCntGroup->setSelected(0); compOnlyPlayersCntGroup->addCallback([&](int btnId) { mapGenOptions.setCompOnlyPlayerCount(btnId); deactivateButtonsFrom(compOnlyTeamsCntGroup, (btnId == 0 ? 1 : btnId)); validateCompOnlyPlayersCnt(btnId); if(!SEL->isGuest()) updateMapInfo(); }); // Computer only teams compOnlyTeamsCntGroup = new CToggleGroup(0); compOnlyTeamsCntGroup->pos.y += 351; compOnlyTeamsCntGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; addButtonsWithRandToGroup(compOnlyTeamsCntGroup, numberDefs, 0, 6, NUMBERS_WIDTH, 234, 241); deactivateButtonsFrom(compOnlyTeamsCntGroup, 1); compOnlyTeamsCntGroup->addCallback([&](int btnId) { mapGenOptions.setCompOnlyTeamCount(btnId); if(!SEL->isGuest()) updateMapInfo(); }); const int WIDE_BTN_WIDTH = 85; // Water content waterContentGroup = new CToggleGroup(0); waterContentGroup->pos.y += 419; waterContentGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; const std::vector waterContentBtns = {"RANNONE","RANNORM","RANISLD"}; addButtonsWithRandToGroup(waterContentGroup, waterContentBtns, 0, 2, WIDE_BTN_WIDTH, 243, 246); waterContentGroup->addCallback([&](int btnId) { mapGenOptions.setWaterContent(static_cast(btnId)); }); // Monster strength monsterStrengthGroup = new CToggleGroup(0); monsterStrengthGroup->pos.y += 485; monsterStrengthGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; const std::vector monsterStrengthBtns = {"RANWEAK","RANNORM","RANSTRG"}; addButtonsWithRandToGroup(monsterStrengthGroup, monsterStrengthBtns, 0, 2, WIDE_BTN_WIDTH, 248, 251); monsterStrengthGroup->addCallback([&](int btnId) { if (btnId < 0) mapGenOptions.setMonsterStrength(EMonsterStrength::RANDOM); else mapGenOptions.setMonsterStrength(static_cast(btnId + EMonsterStrength::GLOBAL_WEAK)); //value 2 to 4 }); // Show random maps btn showRandMaps = new CButton(Point(54, 535), "RANSHOW", CGI->generaltexth->zelp[252]); // Initialize map info object if(!SEL->isGuest()) updateMapInfo(); } void CRandomMapTab::addButtonsWithRandToGroup(CToggleGroup * group, const std::vector & defs, int nStart, int nEnd, int btnWidth, int helpStartIndex, int helpRandIndex) const { addButtonsToGroup(group, defs, nStart, nEnd, btnWidth, helpStartIndex); // Buttons are relative to button group, TODO better solution? SObjectConstruction obj__i(group); const std::string RANDOM_DEF = "RANRAND"; group->addToggle(CMapGenOptions::RANDOM_SIZE, new CToggleButton(Point(256, 0), RANDOM_DEF, CGI->generaltexth->zelp[helpRandIndex])); group->setSelected(CMapGenOptions::RANDOM_SIZE); } void CRandomMapTab::addButtonsToGroup(CToggleGroup * group, const std::vector & defs, int nStart, int nEnd, int btnWidth, int helpStartIndex) const { // Buttons are relative to button group, TODO better solution? SObjectConstruction obj__i(group); int cnt = nEnd - nStart + 1; for(int i = 0; i < cnt; ++i) { auto button = new CToggleButton(Point(i * btnWidth, 0), defs[i + nStart], CGI->generaltexth->zelp[helpStartIndex + i]); // For blocked state we should use pressed image actually button->setImageOrder(0, 1, 1, 3); group->addToggle(i + nStart, button); } } void CRandomMapTab::deactivateButtonsFrom(CToggleGroup * group, int startId) { logGlobal->infoStream() << "Blocking buttons from " << startId; for(auto toggle : group->buttons) { if (auto button = dynamic_cast(toggle.second)) { if(startId == CMapGenOptions::RANDOM_SIZE || toggle.first < startId) { button->block(false); } else { button->block(true); } } } } void CRandomMapTab::validatePlayersCnt(int playersCnt) { if(playersCnt == CMapGenOptions::RANDOM_SIZE) { return; } if(mapGenOptions.getTeamCount() >= playersCnt) { mapGenOptions.setTeamCount(playersCnt - 1); teamsCntGroup->setSelected(mapGenOptions.getTeamCount()); } if(mapGenOptions.getCompOnlyPlayerCount() > 8 - playersCnt) { mapGenOptions.setCompOnlyPlayerCount(8 - playersCnt); compOnlyPlayersCntGroup->setSelected(mapGenOptions.getCompOnlyPlayerCount()); } validateCompOnlyPlayersCnt(mapGenOptions.getCompOnlyPlayerCount()); } void CRandomMapTab::validateCompOnlyPlayersCnt(int compOnlyPlayersCnt) { if(compOnlyPlayersCnt == CMapGenOptions::RANDOM_SIZE) { return; } if(mapGenOptions.getCompOnlyTeamCount() >= compOnlyPlayersCnt) { int compOnlyTeamCount = compOnlyPlayersCnt == 0 ? 0 : compOnlyPlayersCnt - 1; mapGenOptions.setCompOnlyTeamCount(compOnlyTeamCount); compOnlyTeamsCntGroup->setSelected(compOnlyTeamCount); } } std::vector CRandomMapTab::getPossibleMapSizes() { return {CMapHeader::MAP_SIZE_SMALL,CMapHeader::MAP_SIZE_MIDDLE,CMapHeader::MAP_SIZE_LARGE,CMapHeader::MAP_SIZE_XLARGE}; } void CRandomMapTab::showAll(SDL_Surface * to) { CIntObject::showAll(to); // Headline printAtMiddleLoc(CGI->generaltexth->allTexts[738], 222, 36, FONT_BIG, Colors::YELLOW, to); printAtMiddleLoc(CGI->generaltexth->allTexts[739], 222, 56, FONT_SMALL, Colors::WHITE, to); // Map size printAtMiddleLoc(CGI->generaltexth->allTexts[752], 104, 97, FONT_SMALL, Colors::WHITE, to); // Players cnt printAtLoc(CGI->generaltexth->allTexts[753], 68, 133, FONT_SMALL, Colors::WHITE, to); // Teams cnt printAtLoc(CGI->generaltexth->allTexts[754], 68, 199, FONT_SMALL, Colors::WHITE, to); // Computer only players cnt printAtLoc(CGI->generaltexth->allTexts[755], 68, 265, FONT_SMALL, Colors::WHITE, to); // Computer only teams cnt printAtLoc(CGI->generaltexth->allTexts[756], 68, 331, FONT_SMALL, Colors::WHITE, to); // Water content printAtLoc(CGI->generaltexth->allTexts[757], 68, 398, FONT_SMALL, Colors::WHITE, to); // Monster strength printAtLoc(CGI->generaltexth->allTexts[758], 68, 465, FONT_SMALL, Colors::WHITE, to); } void CRandomMapTab::updateMapInfo() { // Generate header info mapInfo = make_unique(); mapInfo->isRandomMap = true; mapInfo->mapHeader = make_unique(); mapInfo->mapHeader->version = EMapFormat::SOD; mapInfo->mapHeader->name = CGI->generaltexth->allTexts[740]; mapInfo->mapHeader->description = CGI->generaltexth->allTexts[741]; mapInfo->mapHeader->difficulty = 1; // Normal mapInfo->mapHeader->height = mapGenOptions.getHeight(); mapInfo->mapHeader->width = mapGenOptions.getWidth(); mapInfo->mapHeader->twoLevel = mapGenOptions.getHasTwoLevels(); // Generate player information mapInfo->mapHeader->players.clear(); int playersToGen = (mapGenOptions.getPlayerCount() == CMapGenOptions::RANDOM_SIZE || mapGenOptions.getCompOnlyPlayerCount() == CMapGenOptions::RANDOM_SIZE) ? 8 : mapGenOptions.getPlayerCount() + mapGenOptions.getCompOnlyPlayerCount(); mapInfo->mapHeader->howManyTeams = playersToGen; for(int i = 0; i < playersToGen; ++i) { PlayerInfo player; player.isFactionRandom = true; player.canComputerPlay = true; if(i >= mapGenOptions.getPlayerCount() && mapGenOptions.getPlayerCount() != CMapGenOptions::RANDOM_SIZE) { player.canHumanPlay = false; } else { player.canHumanPlay = true; } player.team = TeamID(i); player.hasMainTown = true; player.generateHeroAtMainTown = true; mapInfo->mapHeader->players.push_back(player); } mapInfoChanged(mapInfo.get()); } CFunctionList & CRandomMapTab::getMapInfoChanged() { return mapInfoChanged; } const CMapInfo * CRandomMapTab::getMapInfo() const { return mapInfo.get(); } const CMapGenOptions & CRandomMapTab::getMapGenOptions() const { return mapGenOptions; } void CRandomMapTab::setMapGenOptions(shared_ptr opts) { mapSizeBtnGroup->setSelected(vstd::find_pos(getPossibleMapSizes(), opts->getWidth())); twoLevelsBtn->setSelected(opts->getHasTwoLevels()); playersCntGroup->setSelected(opts->getPlayerCount()); teamsCntGroup->setSelected(opts->getTeamCount()); compOnlyPlayersCntGroup->setSelected(opts->getCompOnlyPlayerCount()); compOnlyTeamsCntGroup->setSelected(opts->getCompOnlyTeamCount()); waterContentGroup->setSelected(opts->getWaterContent()); monsterStrengthGroup->setSelected(opts->getMonsterStrength()); } CChatBox::CChatBox(const Rect &rect) { OBJ_CONSTRUCTION; pos += rect; addUsedEvents(KEYBOARD | TEXTINPUT); captureAllKeys = true; type |= REDRAW_PARENT; const int height = graphics->fonts[FONT_SMALL]->getLineHeight(); inputBox = new CTextInput(Rect(0, rect.h - height, rect.w, height)); inputBox->removeUsedEvents(KEYBOARD); chatHistory = new CTextBox("", Rect(0, 0, rect.w, rect.h - height), 1); chatHistory->label->color = Colors::GREEN; } void CChatBox::keyPressed(const SDL_KeyboardEvent & key) { if(key.keysym.sym == SDLK_RETURN && key.state == SDL_PRESSED && inputBox->text.size()) { SEL->postChatMessage(inputBox->text); inputBox->setText(""); } else inputBox->keyPressed(key); } void CChatBox::addNewMessage(const std::string &text) { CCS->soundh->playSound("CHAT"); chatHistory->setText(chatHistory->label->text + text + "\n"); if(chatHistory->slider) chatHistory->slider->moveToMax(); } InfoCard::InfoCard( bool Network ) : bg(nullptr), network(Network), chatOn(false), chat(nullptr), playerListBg(nullptr), difficulty(nullptr), sizes(nullptr), sFlags(nullptr) { OBJ_CONSTRUCTION_CAPTURING_ALL; CIntObject::type |= REDRAW_PARENT; pos.x += 393; pos.y += 6; addUsedEvents(RCLICK); mapDescription = nullptr; Rect descriptionRect(26, 149, 320, 115); mapDescription = new CTextBox("", descriptionRect, 1); if(SEL->screenType == CMenuScreen::campaignList) { CSelectionScreen *ss = static_cast(parent); mapDescription->addChild(new CPicture(*ss->bg, descriptionRect + Point(-393, 0)), true); //move subpicture bg to our description control (by default it's our (Infocard) child) } else { bg = new CPicture("GSELPOP1.bmp", 0, 0); parent->addChild(bg); auto it = vstd::find(parent->children, this); //our position among parent children parent->children.insert(it, bg); //put BG before us parent->children.pop_back(); pos.w = bg->pos.w; pos.h = bg->pos.h; sizes = CDefHandler::giveDef("SCNRMPSZ.DEF"); sFlags = CDefHandler::giveDef("ITGFLAGS.DEF"); difficulty = new CToggleGroup(0); { static const char *difButns[] = {"GSPBUT3.DEF", "GSPBUT4.DEF", "GSPBUT5.DEF", "GSPBUT6.DEF", "GSPBUT7.DEF"}; for(int i = 0; i < 5; i++) { auto button = new CToggleButton(Point(110 + i*32, 450), difButns[i], CGI->generaltexth->zelp[24+i]); difficulty->addToggle(i, button); if(SEL->screenType != CMenuScreen::newGame) button->block(true); } } if(network) { playerListBg = new CPicture("CHATPLUG.bmp", 16, 276); chat = new CChatBox(Rect(26, 132, 340, 132)); chatOn = true; mapDescription->disable(); } } } InfoCard::~InfoCard() { delete sizes; delete sFlags; } void InfoCard::showAll(SDL_Surface * to) { CIntObject::showAll(to); //blit texts if(SEL->screenType != CMenuScreen::campaignList) { printAtLoc(CGI->generaltexth->allTexts[390] + ":", 24, 400, FONT_SMALL, Colors::WHITE, to); //Allies printAtLoc(CGI->generaltexth->allTexts[391] + ":", 190, 400, FONT_SMALL, Colors::WHITE, to); //Enemies printAtLoc(CGI->generaltexth->allTexts[494], 33, 430, FONT_SMALL, Colors::YELLOW, to);//"Map Diff:" printAtLoc(CGI->generaltexth->allTexts[492] + ":", 133,430, FONT_SMALL, Colors::YELLOW, to); //player difficulty printAtLoc(CGI->generaltexth->allTexts[218] + ":", 290,430, FONT_SMALL, Colors::YELLOW, to); //"Rating:" printAtLoc(CGI->generaltexth->allTexts[495], 26, 22, FONT_SMALL, Colors::YELLOW, to); //Scenario Name: if(!chatOn) { printAtLoc(CGI->generaltexth->allTexts[496], 26, 132, FONT_SMALL, Colors::YELLOW, to); //Scenario Description: printAtLoc(CGI->generaltexth->allTexts[497], 26, 283, FONT_SMALL, Colors::YELLOW, to); //Victory Condition: printAtLoc(CGI->generaltexth->allTexts[498], 26, 339, FONT_SMALL, Colors::YELLOW, to); //Loss Condition: } else //players list { std::map playerNames = SEL->playerNames; int playerSoFar = 0; for (auto i = SEL->sInfo.playerInfos.cbegin(); i != SEL->sInfo.playerInfos.cend(); i++) { if(i->second.playerID != PlayerSettings::PLAYER_AI) { printAtLoc(i->second.name, 24, 285 + playerSoFar++ * graphics->fonts[FONT_SMALL]->getLineHeight(), FONT_SMALL, Colors::WHITE, to); playerNames.erase(i->second.playerID); } } playerSoFar = 0; for (auto i = playerNames.cbegin(); i != playerNames.cend(); i++) { printAtLoc(i->second, 193, 285 + playerSoFar++ * graphics->fonts[FONT_SMALL]->getLineHeight(), FONT_SMALL, Colors::WHITE, to); } } } if(SEL->current) { if(SEL->screenType != CMenuScreen::campaignList) { int temp = -1; if(!chatOn) { CDefHandler * loss = CGP ? CGP->loss : CDefHandler::giveDef("SCNRLOSS.DEF"); CDefHandler * victory = CGP ? CGP->victory : CDefHandler::giveDef("SCNRVICT.DEF"); CMapHeader * header = SEL->current->mapHeader.get(); //victory conditions printAtLoc(header->victoryMessage, 60, 307, FONT_SMALL, Colors::WHITE, to); blitAtLoc(victory->ourImages[header->victoryIconIndex].bitmap, 24, 302, to); //victory cond descr //loss conditoins printAtLoc(header->defeatMessage, 60, 366, FONT_SMALL, Colors::WHITE, to); blitAtLoc(loss->ourImages[header->defeatIconIndex].bitmap, 24, 359, to); //loss cond if (!CGP) { delete loss; delete victory; } } //difficulty assert(SEL->current->mapHeader->difficulty <= 4); std::string &diff = CGI->generaltexth->arraytxt[142 + SEL->current->mapHeader->difficulty]; printAtMiddleLoc(diff, 62, 472, FONT_SMALL, Colors::WHITE, to); //selecting size icon switch (SEL->current->mapHeader->width) { case 36: temp=0; break; case 72: temp=1; break; case 108: temp=2; break; case 144: temp=3; break; default: temp=4; break; } blitAtLoc(sizes->ourImages[temp].bitmap, 318, 22, to); if(SEL->screenType == CMenuScreen::loadGame) printToLoc((static_cast(SEL->current))->date,308,34, FONT_SMALL, Colors::WHITE, to); //print flags int fx = 34 + graphics->fonts[FONT_SMALL]->getStringWidth(CGI->generaltexth->allTexts[390]); int ex = 200 + graphics->fonts[FONT_SMALL]->getStringWidth(CGI->generaltexth->allTexts[391]); TeamID myT; if(playerColor < PlayerColor::PLAYER_LIMIT) myT = SEL->current->mapHeader->players[playerColor.getNum()].team; else myT = TeamID::NO_TEAM; for (auto i = SEL->sInfo.playerInfos.cbegin(); i != SEL->sInfo.playerInfos.cend(); i++) { int *myx = ((i->first == playerColor || SEL->current->mapHeader->players[i->first.getNum()].team == myT) ? &fx : &ex); blitAtLoc(sFlags->ourImages[i->first.getNum()].bitmap, *myx, 399, to); *myx += sFlags->ourImages[i->first.getNum()].bitmap->w; } std::string tob; switch (SEL->sInfo.difficulty) { case 0: tob="80%"; break; case 1: tob="100%"; break; case 2: tob="130%"; break; case 3: tob="160%"; break; case 4: tob="200%"; break; } printAtMiddleLoc(tob, 311, 472, FONT_SMALL, Colors::WHITE, to); } //blit description std::string name; if (SEL->screenType == CMenuScreen::campaignList) { name = SEL->current->campaignHeader->name; } else { name = SEL->current->mapHeader->name; } //name if (name.length()) printAtLoc(name, 26, 39, FONT_BIG, Colors::YELLOW, to); else printAtLoc("Unnamed", 26, 39, FONT_BIG, Colors::YELLOW, to); } } void InfoCard::changeSelection( const CMapInfo *to ) { if(to && mapDescription) { if (SEL->screenType == CMenuScreen::campaignList) mapDescription->setText(to->campaignHeader->description); else mapDescription->setText(to->mapHeader->description); mapDescription->label->scrollTextTo(0); if (mapDescription->slider) mapDescription->slider->moveToMin(); if(SEL->screenType != CMenuScreen::newGame && SEL->screenType != CMenuScreen::campaignList) { //difficulty->block(true); difficulty->setSelected(SEL->sInfo.difficulty); } } redraw(); } void InfoCard::clickRight( tribool down, bool previousState ) { static const Rect flagArea(19, 397, 335, 23); if(down && SEL->current && isItInLoc(flagArea, GH.current->motion.x, GH.current->motion.y)) showTeamsPopup(); } void InfoCard::showTeamsPopup() { SDL_Surface *bmp = CMessage::drawDialogBox(256, 90 + 50 * SEL->current->mapHeader->howManyTeams); graphics->fonts[FONT_MEDIUM]->renderTextCenter(bmp, CGI->generaltexth->allTexts[657], Colors::YELLOW, Point(128, 30)); for(int i = 0; i < SEL->current->mapHeader->howManyTeams; i++) { std::vector flags; std::string hlp = CGI->generaltexth->allTexts[656]; //Team %d hlp.replace(hlp.find("%d"), 2, boost::lexical_cast(i+1)); graphics->fonts[FONT_SMALL]->renderTextCenter(bmp, hlp, Colors::WHITE, Point(128, 65 + 50 * i)); for(int j = 0; j < PlayerColor::PLAYER_LIMIT_I; j++) if((SEL->current->mapHeader->players[j].canHumanPlay || SEL->current->mapHeader->players[j].canComputerPlay) && SEL->current->mapHeader->players[j].team == TeamID(i)) flags.push_back(j); int curx = 128 - 9*flags.size(); for(auto & flag : flags) { blitAt(sFlags->ourImages[flag].bitmap, curx, 75 + 50*i, bmp); curx += 18; } } GH.pushInt(new CInfoPopup(bmp, true)); } void InfoCard::toggleChat() { setChat(!chatOn); } void InfoCard::setChat(bool activateChat) { if(chatOn == activateChat) return; assert(active); if(activateChat) { mapDescription->disable(); chat->enable(); playerListBg->enable(); } else { mapDescription->enable(); chat->disable(); playerListBg->disable(); } chatOn = activateChat; GH.totalRedraw(); } OptionsTab::OptionsTab(): turnDuration(nullptr) { OBJ_CONSTRUCTION; bg = new CPicture("ADVOPTBK", 0, 6); pos = bg->pos; if(SEL->screenType == CMenuScreen::newGame) turnDuration = new CSlider(Point(55, 551), 194, std::bind(&OptionsTab::setTurnLength, this, _1), 1, 11, 11, true, CSlider::BLUE); } OptionsTab::~OptionsTab() { } void OptionsTab::showAll(SDL_Surface * to) { CIntObject::showAll(to); printAtMiddleLoc(CGI->generaltexth->allTexts[515], 222, 30, FONT_BIG, Colors::YELLOW, to); printAtMiddleWBLoc(CGI->generaltexth->allTexts[516], 222, 68, FONT_SMALL, 300, Colors::WHITE, to); //Select starting options, handicap, and name for each player in the game. printAtMiddleWBLoc(CGI->generaltexth->allTexts[517], 107, 110, FONT_SMALL, 100, Colors::YELLOW, to); //Player Name Handicap Type printAtMiddleWBLoc(CGI->generaltexth->allTexts[518], 197, 110, FONT_SMALL, 70, Colors::YELLOW, to); //Starting Town printAtMiddleWBLoc(CGI->generaltexth->allTexts[519], 273, 110, FONT_SMALL, 70, Colors::YELLOW, to); //Starting Hero printAtMiddleWBLoc(CGI->generaltexth->allTexts[520], 349, 110, FONT_SMALL, 70, Colors::YELLOW, to); //Starting Bonus printAtMiddleLoc(CGI->generaltexth->allTexts[521], 222, 538, FONT_SMALL, Colors::YELLOW, to); // Player Turn Duration if (turnDuration) printAtMiddleLoc(CGI->generaltexth->turnDurations[turnDuration->getValue()], 319,559, FONT_SMALL, Colors::WHITE, to);//Turn duration value } void OptionsTab::nextCastle( PlayerColor player, int dir ) { if(SEL->isGuest()) { SEL->postRequest(RequestOptionsChange::TOWN, dir); return; } PlayerSettings &s = SEL->sInfo.playerInfos[player]; si16 &cur = s.castle; auto & allowed = SEL->current->mapHeader->players[s.color.getNum()].allowedFactions; if (cur == PlayerSettings::NONE) //no change return; if (cur == PlayerSettings::RANDOM) //first/last available { if (dir > 0) cur = *allowed.begin(); //id of first town else cur = *allowed.rbegin(); //id of last town } else // next/previous available { if ( (cur == *allowed.begin() && dir < 0 ) || (cur == *allowed.rbegin() && dir > 0) ) cur = -1; else { assert(dir >= -1 && dir <= 1); //othervice std::advance may go out of range auto iter = allowed.find(cur); std::advance(iter, dir); cur = *iter; } } if(s.hero >= 0 && !SEL->current->mapHeader->players[s.color.getNum()].hasCustomMainHero()) // remove hero unless it set to fixed one in map editor { usedHeroes.erase(s.hero); // restore previously selected hero back to available pool s.hero = PlayerSettings::RANDOM; } if(cur < 0 && s.bonus == PlayerSettings::RESOURCE) s.bonus = PlayerSettings::RANDOM; entries[player]->selectButtons(); SEL->propagateOptions(); entries[player]->update(); redraw(); } void OptionsTab::nextHero( PlayerColor player, int dir ) { if(SEL->isGuest()) { SEL->postRequest(RequestOptionsChange::HERO, dir); return; } PlayerSettings &s = SEL->sInfo.playerInfos[player]; int old = s.hero; if (s.castle < 0 || s.playerID == PlayerSettings::PLAYER_AI || s.hero == PlayerSettings::NONE) return; if (s.hero == PlayerSettings::RANDOM) // first/last available { int max = CGI->heroh->heroes.size(), min = 0; s.hero = nextAllowedHero(player, min,max,0,dir); } else { if(dir > 0) s.hero = nextAllowedHero(player, s.hero, CGI->heroh->heroes.size(), 1, dir); else s.hero = nextAllowedHero(player, -1, s.hero, 1, dir); // min needs to be -1 -- hero at index 0 would be skipped otherwise } if(old != s.hero) { usedHeroes.erase(old); usedHeroes.insert(s.hero); entries[player]->update(); redraw(); } SEL->propagateOptions(); } int OptionsTab::nextAllowedHero( PlayerColor player, int min, int max, int incl, int dir ) { if(dir>0) { for(int i=min+incl; i<=max-incl; i++) if(canUseThisHero(player, i)) return i; } else { for(int i=max-incl; i>=min+incl; i--) if(canUseThisHero(player, i)) return i; } return -1; } bool OptionsTab::canUseThisHero( PlayerColor player, int ID ) { return CGI->heroh->heroes.size() > ID && SEL->sInfo.playerInfos[player].castle == CGI->heroh->heroes[ID]->heroClass->faction && !vstd::contains(usedHeroes, ID) && SEL->current->mapHeader->allowedHeroes[ID]; } void OptionsTab::nextBonus( PlayerColor player, int dir ) { if(SEL->isGuest()) { SEL->postRequest(RequestOptionsChange::BONUS, dir); return; } PlayerSettings &s = SEL->sInfo.playerInfos[player]; PlayerSettings::Ebonus &ret = s.bonus = static_cast(static_cast(s.bonus) + dir); if (s.hero==PlayerSettings::NONE && !SEL->current->mapHeader->players[s.color.getNum()].heroesNames.size() && ret==PlayerSettings::ARTIFACT) //no hero - can't be artifact { if (dir<0) ret=PlayerSettings::RANDOM; else ret=PlayerSettings::GOLD; } if(ret > PlayerSettings::RESOURCE) ret = PlayerSettings::RANDOM; if(ret < PlayerSettings::RANDOM) ret = PlayerSettings::RESOURCE; if (s.castle==PlayerSettings::RANDOM && ret==PlayerSettings::RESOURCE) //random castle - can't be resource { if (dir<0) ret=PlayerSettings::GOLD; else ret=PlayerSettings::RANDOM; } SEL->propagateOptions(); entries[player]->update(); redraw(); } void OptionsTab::recreate() { for(auto & elem : entries) { delete elem.second; } entries.clear(); usedHeroes.clear(); OBJ_CONSTRUCTION_CAPTURING_ALL; for(auto it = SEL->sInfo.playerInfos.begin(); it != SEL->sInfo.playerInfos.end(); ++it) { entries.insert(std::make_pair(it->first, new PlayerOptionsEntry(this, it->second))); const std::vector &heroes = SEL->current->mapHeader->players[it->first.getNum()].heroesNames; for(auto & heroe : heroes) usedHeroes.insert(heroe.heroId); } } void OptionsTab::setTurnLength( int npos ) { static const int times[] = {1, 2, 4, 6, 8, 10, 15, 20, 25, 30, 0}; vstd::amin(npos, ARRAY_COUNT(times) - 1); SEL->sInfo.turnTime = times[npos]; redraw(); } void OptionsTab::flagPressed( PlayerColor color ) { PlayerSettings &clicked = SEL->sInfo.playerInfos[color]; PlayerSettings *old = nullptr; if(SEL->playerNames.size() == 1) //single player -> just swap { if(color == playerColor) //that color is already selected, no action needed return; old = &SEL->sInfo.playerInfos[playerColor]; swapPlayers(*old, clicked); } else { //identify clicked player int clickedNameID = clicked.playerID; //human is a number of player, zero means AI if(clickedNameID > 0 && playerToRestore.id == clickedNameID) //player to restore is about to being replaced -> put him back to the old place { PlayerSettings &restPos = SEL->sInfo.playerInfos[playerToRestore.color]; SEL->setPlayer(restPos, playerToRestore.id); playerToRestore.reset(); } int newPlayer; //which player will take clicked position //who will be put here? if(!clickedNameID) //AI player clicked -> if possible replace computer with unallocated player { newPlayer = SEL->getIdOfFirstUnallocatedPlayer(); if(!newPlayer) //no "free" player -> get just first one newPlayer = SEL->playerNames.begin()->first; } else //human clicked -> take next { auto i = SEL->playerNames.find(clickedNameID); //clicked one i++; //player AFTER clicked one if(i != SEL->playerNames.end()) newPlayer = i->first; else newPlayer = 0; //AI if we scrolled through all players } SEL->setPlayer(clicked, newPlayer); //put player //if that player was somewhere else, we need to replace him with computer if(newPlayer) //not AI { for(auto i = SEL->sInfo.playerInfos.begin(); i != SEL->sInfo.playerInfos.end(); i++) { int curNameID = i->second.playerID; if(i->first != color && curNameID == newPlayer) { assert(i->second.playerID); playerToRestore.color = i->first; playerToRestore.id = newPlayer; SEL->setPlayer(i->second, 0); //set computer old = &i->second; break; } } } } entries[clicked.color]->selectButtons(); if(old) { entries[old->color]->selectButtons(); if(old->hero >= 0) usedHeroes.erase(old->hero); old->hero = entries[old->color]->pi.defaultHero(); entries[old->color]->update(); // update previous frame images in case entries were auto-updated } SEL->propagateOptions(); GH.totalRedraw(); } OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry( OptionsTab *owner, PlayerSettings &S) : pi(SEL->current->mapHeader->players[S.color.getNum()]), s(S) { OBJ_CONSTRUCTION; defActions |= SHARE_POS; int serial = 0; for(int g=0; g < s.color.getNum(); ++g) { PlayerInfo &itred = SEL->current->mapHeader->players[g]; if( itred.canComputerPlay || itred.canHumanPlay) serial++; } pos.x += 54; pos.y += 122 + serial*50; static const char *flags[] = {"AOFLGBR.DEF", "AOFLGBB.DEF", "AOFLGBY.DEF", "AOFLGBG.DEF", "AOFLGBO.DEF", "AOFLGBP.DEF", "AOFLGBT.DEF", "AOFLGBS.DEF"}; static const char *bgs[] = {"ADOPRPNL.bmp", "ADOPBPNL.bmp", "ADOPYPNL.bmp", "ADOPGPNL.bmp", "ADOPOPNL.bmp", "ADOPPPNL.bmp", "ADOPTPNL.bmp", "ADOPSPNL.bmp"}; bg = new CPicture(BitmapHandler::loadBitmap(bgs[s.color.getNum()]), 0, 0, true); if(SEL->screenType == CMenuScreen::newGame) { btns[0] = new CButton(Point(107, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[132], std::bind(&OptionsTab::nextCastle, owner, s.color, -1)); btns[1] = new CButton(Point(168, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[133], std::bind(&OptionsTab::nextCastle, owner, s.color, +1)); btns[2] = new CButton(Point(183, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[148], std::bind(&OptionsTab::nextHero, owner, s.color, -1)); btns[3] = new CButton(Point(244, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[149], std::bind(&OptionsTab::nextHero, owner, s.color, +1)); btns[4] = new CButton(Point(259, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[164], std::bind(&OptionsTab::nextBonus, owner, s.color, -1)); btns[5] = new CButton(Point(320, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[165], std::bind(&OptionsTab::nextBonus, owner, s.color, +1)); } else for(auto & elem : btns) elem = nullptr; selectButtons(); assert(SEL->current && SEL->current->mapHeader); const PlayerInfo &p = SEL->current->mapHeader->players[s.color.getNum()]; assert(p.canComputerPlay || p.canHumanPlay); //someone must be able to control this player if(p.canHumanPlay && p.canComputerPlay) whoCanPlay = HUMAN_OR_CPU; else if(p.canComputerPlay) whoCanPlay = CPU; else whoCanPlay = HUMAN; if(SEL->screenType != CMenuScreen::scenarioInfo && SEL->current->mapHeader->players[s.color.getNum()].canHumanPlay) { flag = new CButton(Point(-43, 2), flags[s.color.getNum()], CGI->generaltexth->zelp[180], std::bind(&OptionsTab::flagPressed, owner, s.color)); flag->hoverable = true; flag->block(SEL->isGuest()); } else flag = nullptr; town = new SelectedBox(Point(119, 2), s, TOWN); hero = new SelectedBox(Point(195, 2), s, HERO); bonus = new SelectedBox(Point(271, 2), s, BONUS); } void OptionsTab::PlayerOptionsEntry::showAll(SDL_Surface * to) { CIntObject::showAll(to); printAtMiddleLoc(s.name, 55, 10, FONT_SMALL, Colors::WHITE, to); printAtMiddleWBLoc(CGI->generaltexth->arraytxt[206+whoCanPlay], 28, 39, FONT_TINY, 50, Colors::WHITE, to); } void OptionsTab::PlayerOptionsEntry::update() { town->update(); hero->update(); bonus->update(); } void OptionsTab::PlayerOptionsEntry::selectButtons() { if(!btns[0]) return; if( (pi.defaultCastle() != -1) //fixed tow || (SEL->isGuest() && s.color != playerColor)) //or not our player { btns[0]->disable(); btns[1]->disable(); } else { btns[0]->enable(); btns[1]->enable(); } if( (pi.defaultHero() != -1 || !s.playerID || s.castle < 0) //fixed hero || (SEL->isGuest() && s.color != playerColor))//or not our player { btns[2]->disable(); btns[3]->disable(); } else { btns[2]->enable(); btns[3]->enable(); } if(SEL->isGuest() && s.color != playerColor)//or not our player { btns[4]->disable(); btns[5]->disable(); } else { btns[4]->enable(); btns[5]->enable(); } } size_t OptionsTab::CPlayerSettingsHelper::getImageIndex() { enum EBonusSelection //frames of bonuses file { WOOD_ORE = 0, CRYSTAL = 1, GEM = 2, MERCURY = 3, SULFUR = 5, GOLD = 8, ARTIFACT = 9, RANDOM = 10, WOOD = 0, ORE = 0, MITHRIL = 10, // resources unavailable in bonuses file TOWN_RANDOM = 38, TOWN_NONE = 39, // Special frames in ITPA HERO_RANDOM = 163, HERO_NONE = 164 // Special frames in PortraitsSmall }; switch(type) { case TOWN: switch (settings.castle) { case PlayerSettings::NONE: return TOWN_NONE; case PlayerSettings::RANDOM: return TOWN_RANDOM; default: return CGI->townh->factions[settings.castle]->town->clientInfo.icons[true][false] + 2; } case HERO: switch (settings.hero) { case PlayerSettings::NONE: return HERO_NONE; case PlayerSettings::RANDOM: return HERO_RANDOM; default: { if(settings.heroPortrait >= 0) return settings.heroPortrait; return CGI->heroh->heroes[settings.hero]->imageIndex; } } case BONUS: { switch(settings.bonus) { case PlayerSettings::RANDOM: return RANDOM; case PlayerSettings::ARTIFACT: return ARTIFACT; case PlayerSettings::GOLD: return GOLD; case PlayerSettings::RESOURCE: { switch(CGI->townh->factions[settings.castle]->town->primaryRes) { case Res::WOOD_AND_ORE : return WOOD_ORE; case Res::WOOD : return WOOD; case Res::MERCURY : return MERCURY; case Res::ORE : return ORE; case Res::SULFUR : return SULFUR; case Res::CRYSTAL : return CRYSTAL; case Res::GEMS : return GEM; case Res::GOLD : return GOLD; case Res::MITHRIL : return MITHRIL; } } } } } return 0; } std::string OptionsTab::CPlayerSettingsHelper::getImageName() { switch(type) { case OptionsTab::TOWN: return "ITPA"; case OptionsTab::HERO: return "PortraitsSmall"; case OptionsTab::BONUS: return "SCNRSTAR"; } return ""; } std::string OptionsTab::CPlayerSettingsHelper::getTitle() { switch(type) { case OptionsTab::TOWN: return (settings.castle < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; case OptionsTab::HERO: return (settings.hero < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; case OptionsTab::BONUS: { switch(settings.bonus) { case PlayerSettings::RANDOM: return CGI->generaltexth->allTexts[86]; //{Random Bonus} case PlayerSettings::ARTIFACT: return CGI->generaltexth->allTexts[83]; //{Artifact Bonus} case PlayerSettings::GOLD: return CGI->generaltexth->allTexts[84]; //{Gold Bonus} case PlayerSettings::RESOURCE: return CGI->generaltexth->allTexts[85]; //{Resource Bonus} } } } return ""; } std::string OptionsTab::CPlayerSettingsHelper::getName() { switch(type) { case TOWN: { switch (settings.castle) { case PlayerSettings::NONE : return CGI->generaltexth->allTexts[523]; case PlayerSettings::RANDOM : return CGI->generaltexth->allTexts[522]; default : return CGI->townh->factions[settings.castle]->name; } } case HERO: { switch (settings.hero) { case PlayerSettings::NONE : return CGI->generaltexth->allTexts[523]; case PlayerSettings::RANDOM : return CGI->generaltexth->allTexts[522]; default : { if (!settings.heroName.empty()) return settings.heroName; return CGI->heroh->heroes[settings.hero]->name; } } } case BONUS: { switch (settings.bonus) { case PlayerSettings::RANDOM : return CGI->generaltexth->allTexts[522]; default: return CGI->generaltexth->arraytxt[214 + settings.bonus]; } } } return ""; } std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() { switch(type) { case TOWN: return getName(); case HERO: { if (settings.hero >= 0) return getName() + " - " + CGI->heroh->heroes[settings.hero]->heroClass->name; return getName(); } case BONUS: { switch(settings.bonus) { case PlayerSettings::GOLD: return CGI->generaltexth->allTexts[87]; //500-1000 case PlayerSettings::RESOURCE: { switch(CGI->townh->factions[settings.castle]->town->primaryRes) { case Res::MERCURY: return CGI->generaltexth->allTexts[694]; case Res::SULFUR: return CGI->generaltexth->allTexts[695]; case Res::CRYSTAL: return CGI->generaltexth->allTexts[692]; case Res::GEMS: return CGI->generaltexth->allTexts[693]; case Res::WOOD_AND_ORE: return CGI->generaltexth->allTexts[89]; //At the start of the game, 5-10 wood and 5-10 ore are added to your Kingdom's resource pool } } } } } return ""; } std::string OptionsTab::CPlayerSettingsHelper::getDescription() { switch(type) { case TOWN: return CGI->generaltexth->allTexts[104]; case HERO: return CGI->generaltexth->allTexts[102]; case BONUS: { switch(settings.bonus) { case PlayerSettings::RANDOM: return CGI->generaltexth->allTexts[94]; //Gold, wood and ore, or an artifact is randomly chosen as your starting bonus case PlayerSettings::ARTIFACT: return CGI->generaltexth->allTexts[90]; //An artifact is randomly chosen and equipped to your starting hero case PlayerSettings::GOLD: return CGI->generaltexth->allTexts[92]; //At the start of the game, 500-1000 gold is added to your Kingdom's resource pool case PlayerSettings::RESOURCE: { switch(CGI->townh->factions[settings.castle]->town->primaryRes) { case Res::MERCURY: return CGI->generaltexth->allTexts[690]; case Res::SULFUR: return CGI->generaltexth->allTexts[691]; case Res::CRYSTAL: return CGI->generaltexth->allTexts[688]; case Res::GEMS: return CGI->generaltexth->allTexts[689]; case Res::WOOD_AND_ORE: return CGI->generaltexth->allTexts[93]; //At the start of the game, 5-10 wood and 5-10 ore are added to your Kingdom's resource pool } } } } } return ""; } OptionsTab::CPregameTooltipBox::CPregameTooltipBox(CPlayerSettingsHelper & helper): CWindowObject(BORDERED | RCLICK_POPUP), CPlayerSettingsHelper(helper) { OBJ_CONSTRUCTION_CAPTURING_ALL; int value = PlayerSettings::NONE; switch(CPlayerSettingsHelper::type) { break; case TOWN: value = settings.castle; break; case HERO: value = settings.hero; break; case BONUS: value = settings.bonus; } if (value == PlayerSettings::RANDOM) genBonusWindow(); else if (CPlayerSettingsHelper::type == BONUS) genBonusWindow(); else if (CPlayerSettingsHelper::type == HERO) genHeroWindow(); else if (CPlayerSettingsHelper::type == TOWN) genTownWindow(); center(); } void OptionsTab::CPregameTooltipBox::genHeader() { new CFilledTexture("DIBOXBCK", pos); updateShadow(); new CLabel(pos.w / 2 + 8, 21, FONT_MEDIUM, CENTER, Colors::YELLOW, getTitle()); new CLabel(pos.w / 2, 88, FONT_SMALL, CENTER, Colors::WHITE, getSubtitle()); new CAnimImage(getImageName(), getImageIndex(), 0, pos.w / 2 - 24, 45); } void OptionsTab::CPregameTooltipBox::genTownWindow() { pos = Rect(0, 0, 228, 290); genHeader(); new CLabel(pos.w / 2 + 8, 122, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[79]); std::vector components; const CTown * town = CGI->townh->factions[settings.castle]->town; for (auto & elem : town->creatures) { if (!elem.empty()) components.push_back(new CComponent(CComponent::creature, elem.front(), 0, CComponent::tiny)); } new CComponentBox(components, Rect(10, 140, pos.w - 20, 140)); } void OptionsTab::CPregameTooltipBox::genHeroWindow() { pos = Rect(0, 0, 292, 226); genHeader(); // specialty new CAnimImage("UN44", CGI->heroh->heroes[settings.hero]->imageIndex, 0, pos.w / 2 - 22, 134); new CLabel(pos.w / 2 + 4, 117, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); new CLabel(pos.w / 2, 188, FONT_SMALL, CENTER, Colors::WHITE, CGI->heroh->heroes[settings.hero]->specName); } void OptionsTab::CPregameTooltipBox::genBonusWindow() { pos = Rect(0, 0, 228, 162); genHeader(); new CTextBox(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, CENTER, Colors::WHITE ); } OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, SelType type) :CIntObject(RCLICK, position), CPlayerSettingsHelper(settings, type) { OBJ_CONSTRUCTION_CAPTURING_ALL; image = new CAnimImage(getImageName(), getImageIndex()); subtitle = new CLabel(23, 39, FONT_TINY, CENTER, Colors::WHITE, getName()); pos = image->pos; } void OptionsTab::SelectedBox::update() { image->setFrame(getImageIndex()); subtitle->setText(getName()); } void OptionsTab::SelectedBox::clickRight( tribool down, bool previousState ) { if (down) { // cases when we do not need to display a message if (settings.castle == -2 && CPlayerSettingsHelper::type == TOWN ) return; if (settings.hero == -2 && !SEL->current->mapHeader->players[settings.color.getNum()].hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) return; GH.pushInt(new CPregameTooltipBox(*this)); } } CScenarioInfo::CScenarioInfo(const CMapHeader *mapHeader, const StartInfo *startInfo) { OBJ_CONSTRUCTION_CAPTURING_ALL; for(auto it = startInfo->playerInfos.cbegin(); it != startInfo->playerInfos.cend(); ++it) { if(it->second.playerID) { playerColor = it->first; } } pos.w = 762; pos.h = 584; center(pos); assert(LOCPLINT); sInfo = *LOCPLINT->cb->getStartInfo(); assert(!SEL->current); current = mapInfoFromGame(); setPlayersFromGame(); screenType = CMenuScreen::scenarioInfo; card = new InfoCard(); opt = new OptionsTab(); opt->recreate(); card->changeSelection(current); card->difficulty->setSelected(startInfo->difficulty); back = new CButton(Point(584, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], std::bind(&CGuiHandler::popIntTotally, &GH, this), SDLK_ESCAPE); } CScenarioInfo::~CScenarioInfo() { delete current; } bool mapSorter::operator()(const CMapInfo *aaa, const CMapInfo *bbb) { const CMapHeader * a = aaa->mapHeader.get(), * b = bbb->mapHeader.get(); if(a && b) //if we are sorting scenarios { switch (sortBy) { case _format: //by map format (RoE, WoG, etc) return (a->versionversion); break; case _loscon: //by loss conditions return (a->defeatMessage < b->defeatMessage); break; case _playerAm: //by player amount int playerAmntB,humenPlayersB,playerAmntA,humenPlayersA; playerAmntB=humenPlayersB=playerAmntA=humenPlayersA=0; for (int i=0;i<8;i++) { if (a->players[i].canHumanPlay) {playerAmntA++;humenPlayersA++;} else if (a->players[i].canComputerPlay) {playerAmntA++;} if (b->players[i].canHumanPlay) {playerAmntB++;humenPlayersB++;} else if (b->players[i].canComputerPlay) {playerAmntB++;} } if (playerAmntB!=playerAmntA) return (playerAmntAwidthwidth); break; case _viccon: //by victory conditions return (a->victoryMessage < b->victoryMessage); break; case _name: //by name return boost::ilexicographical_compare(a->name, b->name); case _fileName: //by filename return boost::ilexicographical_compare(aaa->fileURI, bbb->fileURI); default: return boost::ilexicographical_compare(a->name, b->name); } } else //if we are sorting campaigns { switch(sortBy) { case _numOfMaps: //by number of maps in campaign return CGI->generaltexth->campaignRegionNames[ aaa->campaignHeader->mapVersion ].size() < CGI->generaltexth->campaignRegionNames[ bbb->campaignHeader->mapVersion ].size(); break; case _name: //by name return boost::ilexicographical_compare(aaa->campaignHeader->name, bbb->campaignHeader->name); default: return boost::ilexicographical_compare(aaa->campaignHeader->name, bbb->campaignHeader->name); } } } CMultiMode::CMultiMode() { OBJ_CONSTRUCTION_CAPTURING_ALL; bg = new CPicture("MUPOPUP.bmp"); bg->convertToScreenBPP(); //so we could draw without problems blitAt(CPicture("MUMAP.bmp"), 16, 77, *bg); //blit img pos = bg->center(); //center, window has size of bg graphic bar = new CGStatusBar(new CPicture(Rect(7, 465, 440, 18), 0));//226, 472 txt = new CTextInput(Rect(19, 436, 334, 16), *bg); txt->setText(settings["general"]["playerName"].String()); //Player btns[0] = new CButton(Point(373, 78), "MUBHOT.DEF", CGI->generaltexth->zelp[266], std::bind(&CMultiMode::openHotseat, this)); btns[1] = new CButton(Point(373, 78 + 57*1), "MUBHOST.DEF", CButton::tooltip("Host TCP/IP game", ""), std::bind(&CMultiMode::hostTCP, this)); btns[2] = new CButton(Point(373, 78 + 57*2), "MUBJOIN.DEF", CButton::tooltip("Join TCP/IP game", ""), std::bind(&CMultiMode::joinTCP, this)); btns[6] = new CButton(Point(373, 424), "MUBCANC.DEF", CGI->generaltexth->zelp[288], [&] { GH.popIntTotally(this);}, SDLK_ESCAPE); } void CMultiMode::openHotseat() { GH.pushInt(new CHotSeatPlayers(txt->text)); } void CMultiMode::hostTCP() { Settings name = settings.write["general"]["playerName"]; name->String() = txt->text; GH.popIntTotally(this); GH.pushInt(new CSelectionScreen(CMenuScreen::newGame, CMenuScreen::MULTI_NETWORK_HOST)); } void CMultiMode::joinTCP() { Settings name = settings.write["general"]["playerName"]; name->String() = txt->text; GH.pushInt(new CSimpleJoinScreen); } CHotSeatPlayers::CHotSeatPlayers(const std::string &firstPlayer) { OBJ_CONSTRUCTION_CAPTURING_ALL; bg = new CPicture("MUHOTSEA.bmp"); pos = bg->center(); //center, window has size of bg graphic std::string text = CGI->generaltexth->allTexts[446]; boost::replace_all(text, "\t","\n"); Rect boxRect(25, 20, 315, 50); title = new CTextBox(text, boxRect, 0, FONT_BIG, CENTER, Colors::WHITE);//HOTSEAT Please enter names for(int i = 0; i < ARRAY_COUNT(txt); i++) { txt[i] = new CTextInput(Rect(60, 85 + i*30, 280, 16), *bg); txt[i]->cb += std::bind(&CHotSeatPlayers::onChange, this, _1); } ok = new CButton(Point(95, 338), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CHotSeatPlayers::enterSelectionScreen, this), SDLK_RETURN); cancel = new CButton(Point(205, 338), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CGuiHandler::popIntTotally, std::ref(GH), this), SDLK_ESCAPE); bar = new CGStatusBar(new CPicture(Rect(7, 381, 348, 18), 0));//226, 472 txt[0]->setText(firstPlayer, true); txt[0]->giveFocus(); } void CHotSeatPlayers::onChange(std::string newText) { size_t namesCount = 0; for(auto & elem : txt) if(!elem->text.empty()) namesCount++; ok->block(namesCount < 2); } void CHotSeatPlayers::enterSelectionScreen() { std::map names; for(int i = 0, j = 1; i < ARRAY_COUNT(txt); i++) if(txt[i]->text.length()) names[j++] = txt[i]->text; Settings name = settings.write["general"]["playerName"]; name->String() = names.begin()->second; GH.popInts(2); //pop MP mode window and this GH.pushInt(new CSelectionScreen(CMenuScreen::newGame, CMenuScreen::MULTI_HOT_SEAT, &names)); } void CBonusSelection::init() { highlightedRegion = nullptr; ourHeader = nullptr; diffLb = nullptr; diffRb = nullptr; bonuses = nullptr; selectedMap = 0; // Initialize start info startInfo.mapname = ourCampaign->camp->header.filename; startInfo.mode = StartInfo::CAMPAIGN; startInfo.campState = ourCampaign; startInfo.turnTime = 0; OBJ_CONSTRUCTION_CAPTURING_ALL; static const std::string bgNames [] = {"E1_BG.BMP", "G2_BG.BMP", "E2_BG.BMP", "G1_BG.BMP", "G3_BG.BMP", "N1_BG.BMP", "S1_BG.BMP", "BR_BG.BMP", "IS_BG.BMP", "KR_BG.BMP", "NI_BG.BMP", "TA_BG.BMP", "AR_BG.BMP", "HS_BG.BMP", "BB_BG.BMP", "NB_BG.BMP", "EL_BG.BMP", "RN_BG.BMP", "UA_BG.BMP", "SP_BG.BMP"}; loadPositionsOfGraphics(); background = BitmapHandler::loadBitmap(bgNames[ourCampaign->camp->header.mapVersion]); pos.h = background->h; pos.w = background->w; center(); SDL_Surface * panel = BitmapHandler::loadBitmap("CAMPBRF.BMP"); blitAt(panel, 456, 6, background); startB = new CButton(Point(475, 536), "CBBEGIB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), SDLK_RETURN); restartB = new CButton(Point(475, 536), "CBRESTB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), SDLK_RETURN); backB = new CButton(Point(624, 536), "CBCANCB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), SDLK_ESCAPE); //campaign name if (ourCampaign->camp->header.name.length()) graphics->fonts[FONT_BIG]->renderTextLeft(background, ourCampaign->camp->header.name, Colors::YELLOW, Point(481, 28)); else graphics->fonts[FONT_BIG]->renderTextLeft(background, CGI->generaltexth->allTexts[508], Colors::YELLOW, Point(481, 28)); //map size icon sizes = CDefHandler::giveDef("SCNRMPSZ.DEF"); //campaign description graphics->fonts[FONT_SMALL]->renderTextLeft(background, CGI->generaltexth->allTexts[38], Colors::YELLOW, Point(481, 63)); campaignDescription = new CTextBox(ourCampaign->camp->header.description, Rect(480, 86, 286, 117), 1); //campaignDescription->showAll(background); //map description mapDescription = new CTextBox("", Rect(480, 280, 286, 117), 1); //bonus choosing graphics->fonts[FONT_MEDIUM]->renderTextLeft(background, CGI->generaltexth->allTexts[71], Colors::WHITE, Point(511, 432)); bonuses = new CToggleGroup(std::bind(&CBonusSelection::selectBonus, this, _1)); //set left part of window bool isCurrentMapConquerable = ourCampaign->currentMap && ourCampaign->camp->conquerable(*ourCampaign->currentMap); for(int g = 0; g < ourCampaign->camp->scenarios.size(); ++g) { if(ourCampaign->camp->conquerable(g)) { regions.push_back(new CRegion(this, true, true, g)); regions[regions.size()-1]->rclickText = ourCampaign->camp->scenarios[g].regionText; if(highlightedRegion == nullptr) { if(!isCurrentMapConquerable || (isCurrentMapConquerable && g == *ourCampaign->currentMap)) { highlightedRegion = regions.back(); selectMap(g, true); } } } else if (ourCampaign->camp->scenarios[g].conquered) //display as striped { regions.push_back(new CRegion(this, false, false, g)); regions[regions.size()-1]->rclickText = ourCampaign->camp->scenarios[g].regionText; } } //allies / enemies graphics->fonts[FONT_SMALL]->renderTextLeft(background, CGI->generaltexth->allTexts[390] + ":", Colors::WHITE, Point(486, 407)); graphics->fonts[FONT_SMALL]->renderTextLeft(background, CGI->generaltexth->allTexts[391] + ":", Colors::WHITE, Point(619, 407)); SDL_FreeSurface(panel); //difficulty std::vector difficulty; boost::split(difficulty, CGI->generaltexth->allTexts[492], boost::is_any_of(" ")); graphics->fonts[FONT_MEDIUM]->renderTextLeft(background, difficulty.back(), Colors::WHITE, Point(689, 432)); //difficulty pics for (int b=0; b(b+3) + ".DEF"); SDL_Surface * surfToDuplicate = cde->ourImages[0].bitmap; diffPics[b] = SDL_ConvertSurface(surfToDuplicate, surfToDuplicate->format, surfToDuplicate->flags); delete cde; } //difficulty selection buttons if (ourCampaign->camp->header.difficultyChoosenByPlayer) { diffLb = new CButton(Point(694, 508), "SCNRBLF.DEF", CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); diffRb = new CButton(Point(738, 508), "SCNRBRT.DEF", CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); } //load miniflags sFlags = CDefHandler::giveDef("ITGFLAGS.DEF"); } CBonusSelection::CBonusSelection(shared_ptr _ourCampaign) : ourCampaign(_ourCampaign) { init(); } CBonusSelection::CBonusSelection(const std::string & campaignFName) { ourCampaign = make_shared(CCampaignHandler::getCampaign(campaignFName)); init(); } CBonusSelection::~CBonusSelection() { SDL_FreeSurface(background); delete sizes; delete ourHeader; delete sFlags; for (auto & elem : diffPics) { SDL_FreeSurface(elem); } } void CBonusSelection::goBack() { GH.popIntTotally(this); } void CBonusSelection::showAll(SDL_Surface * to) { blitAt(background, pos.x, pos.y, to); CIntObject::showAll(to); show(to); if (pos.h != to->h || pos.w != to->w) CMessage::drawBorder(PlayerColor(1), to, pos.w+28, pos.h+30, pos.x-14, pos.y-15); } void CBonusSelection::loadPositionsOfGraphics() { const JsonNode config(ResourceID("config/campaign_regions.json")); int idx = 0; for(const JsonNode &campaign : config["campaign_regions"].Vector()) { SCampPositions sc; sc.campPrefix = campaign["prefix"].String(); sc.colorSuffixLength = campaign["color_suffix_length"].Float(); for(const JsonNode &desc : campaign["desc"].Vector()) { SCampPositions::SRegionDesc rd; rd.infix = desc["infix"].String(); rd.xpos = desc["x"].Float(); rd.ypos = desc["y"].Float(); sc.regions.push_back(rd); } campDescriptions.push_back(sc); idx++; } assert(idx == CGI->generaltexth->campaignMapNames.size()); } void CBonusSelection::selectMap(int mapNr, bool initialSelect) { if(initialSelect || selectedMap != mapNr) { // initialize restart / start button if(!ourCampaign->currentMap || *ourCampaign->currentMap != mapNr) { // draw start button restartB->disable(); startB->enable(); if (!ourCampaign->mapsConquered.empty()) backB->block(true); else backB->block(false); } else { // draw restart button startB->disable(); restartB->enable(); backB->block(false); } startInfo.difficulty = ourCampaign->camp->scenarios[mapNr].difficulty; selectedMap = mapNr; selectedBonus = boost::none; std::string scenarioName = ourCampaign->camp->header.filename.substr(0, ourCampaign->camp->header.filename.find('.')); boost::to_lower(scenarioName); scenarioName += ':' + boost::lexical_cast(selectedMap); //get header delete ourHeader; std::string & headerStr = ourCampaign->camp->mapPieces.find(mapNr)->second; auto buffer = reinterpret_cast(headerStr.data()); ourHeader = CMapService::loadMapHeader(buffer, headerStr.size(), scenarioName).release(); std::map names; names[1] = settings["general"]["playerName"].String(); updateStartInfo(ourCampaign->camp->header.filename, startInfo, ourHeader, names); mapDescription->setText(ourHeader->description); updateBonusSelection(); GH.totalRedraw(); } } void CBonusSelection::show(SDL_Surface * to) { //blitAt(background, pos.x, pos.y, to); //map name std::string mapName = ourHeader->name; if (mapName.length()) printAtLoc(mapName, 481, 219, FONT_BIG, Colors::YELLOW, to); else printAtLoc("Unnamed", 481, 219, FONT_BIG, Colors::YELLOW, to); //map description printAtLoc(CGI->generaltexth->allTexts[496], 481, 253, FONT_SMALL, Colors::YELLOW, to); mapDescription->showAll(to); //showAll because CTextBox has no show() //map size icon int temp; switch (ourHeader->width) { case 36: temp=0; break; case 72: temp=1; break; case 108: temp=2; break; case 144: temp=3; break; default: temp=4; break; } blitAtLoc(sizes->ourImages[temp].bitmap, 735, 26, to); //flags int fx = 496 + graphics->fonts[FONT_SMALL]->getStringWidth(CGI->generaltexth->allTexts[390]); int ex = 629 + graphics->fonts[FONT_SMALL]->getStringWidth(CGI->generaltexth->allTexts[391]); TeamID myT; myT = ourHeader->players[playerColor.getNum()].team; for (auto i = startInfo.playerInfos.cbegin(); i != startInfo.playerInfos.cend(); i++) { int *myx = ((i->first == playerColor || ourHeader->players[i->first.getNum()].team == myT) ? &fx : &ex); blitAtLoc(sFlags->ourImages[i->first.getNum()].bitmap, *myx, 405, to); *myx += sFlags->ourImages[i->first.getNum()].bitmap->w; } //difficulty blitAtLoc(diffPics[startInfo.difficulty], 709, 455, to); CIntObject::show(to); } void CBonusSelection::updateBonusSelection() { OBJ_CONSTRUCTION_CAPTURING_ALL; //graphics: //spell - SPELLBON.DEF //monster - TWCRPORT.DEF //building - BO*.BMP graphics //artifact - ARTIFBON.DEF //spell scroll - SPELLBON.DEF //prim skill - PSKILBON.DEF //sec skill - SSKILBON.DEF //resource - BORES.DEF //player - CREST58.DEF //hero - PORTRAITSLARGE (HPL###.BMPs) const CCampaignScenario &scenario = ourCampaign->camp->scenarios[selectedMap]; const std::vector & bonDescs = scenario.travelOptions.bonusesToChoose; updateStartButtonState(-1); delete bonuses; bonuses = new CToggleGroup(std::bind(&CBonusSelection::selectBonus, this, _1)); static const char *bonusPics[] = {"SPELLBON.DEF", "TWCRPORT.DEF", "", "ARTIFBON.DEF", "SPELLBON.DEF", "PSKILBON.DEF", "SSKILBON.DEF", "BORES.DEF", "PORTRAITSLARGE", "PORTRAITSLARGE"}; for(int i = 0; i < bonDescs.size(); i++) { std::string picName=bonusPics[bonDescs[i].type]; size_t picNumber=bonDescs[i].info2; std::string desc; switch(bonDescs[i].type) { case CScenarioTravel::STravelBonus::SPELL: desc = CGI->generaltexth->allTexts[715]; boost::algorithm::replace_first(desc, "%s", CGI->spellh->objects[bonDescs[i].info2]->name); break; case CScenarioTravel::STravelBonus::MONSTER: picNumber = bonDescs[i].info2 + 2; desc = CGI->generaltexth->allTexts[717]; boost::algorithm::replace_first(desc, "%d", boost::lexical_cast(bonDescs[i].info3)); boost::algorithm::replace_first(desc, "%s", CGI->creh->creatures[bonDescs[i].info2]->namePl); break; case CScenarioTravel::STravelBonus::BUILDING: { int faction = -1; for(auto & elem : startInfo.playerInfos) { if (elem.second.playerID) { faction = elem.second.castle; break; } } assert(faction != -1); BuildingID buildID = CBuildingHandler::campToERMU(bonDescs[i].info1, faction, std::set()); picName = graphics->ERMUtoPicture[faction][buildID]; picNumber = -1; if (vstd::contains(CGI->townh->factions[faction]->town->buildings, buildID)) desc = CGI->townh->factions[faction]->town->buildings.find(buildID)->second->Name(); } break; case CScenarioTravel::STravelBonus::ARTIFACT: desc = CGI->generaltexth->allTexts[715]; boost::algorithm::replace_first(desc, "%s", CGI->arth->artifacts[bonDescs[i].info2]->Name()); break; case CScenarioTravel::STravelBonus::SPELL_SCROLL: desc = CGI->generaltexth->allTexts[716]; boost::algorithm::replace_first(desc, "%s", CGI->spellh->objects[bonDescs[i].info2]->name); break; case CScenarioTravel::STravelBonus::PRIMARY_SKILL: { int leadingSkill = -1; std::vector > toPrint; //primary skills to be listed const ui8* ptr = reinterpret_cast(&bonDescs[i].info2); for (int g=0; g ptr[leadingSkill]) { leadingSkill = g; } if (ptr[g] != 0) { toPrint.push_back(std::make_pair(g, ptr[g])); } } picNumber = leadingSkill; desc = CGI->generaltexth->allTexts[715]; std::string substitute; //text to be printed instead of %s for (int v=0; v(toPrint[v].second); substitute += " " + CGI->generaltexth->primarySkillNames[toPrint[v].first]; if(v != toPrint.size() - 1) { substitute += ", "; } } boost::algorithm::replace_first(desc, "%s", substitute); break; } case CScenarioTravel::STravelBonus::SECONDARY_SKILL: desc = CGI->generaltexth->allTexts[718]; boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->levels[bonDescs[i].info3 - 1]); //skill level boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->skillName[bonDescs[i].info2]); //skill name picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1; break; case CScenarioTravel::STravelBonus::RESOURCE: { int serialResID = 0; switch(bonDescs[i].info1) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: serialResID = bonDescs[i].info1; break; case 0xFD: //wood + ore serialResID = 7; break; case 0xFE: //rare resources serialResID = 8; break; } picNumber = serialResID; desc = CGI->generaltexth->allTexts[717]; boost::algorithm::replace_first(desc, "%d", boost::lexical_cast(bonDescs[i].info2)); std::string replacement; if (serialResID <= 6) { replacement = CGI->generaltexth->restypes[serialResID]; } else { replacement = CGI->generaltexth->allTexts[714 + serialResID]; } boost::algorithm::replace_first(desc, "%s", replacement); } break; case CScenarioTravel::STravelBonus::HEROES_FROM_PREVIOUS_SCENARIO: { auto superhero = ourCampaign->camp->scenarios[bonDescs[i].info2].strongestHero(PlayerColor(bonDescs[i].info1)); if (!superhero) logGlobal->warnStream() << "No superhero! How could it be transferred?"; picNumber = superhero ? superhero->portrait : 0; desc = CGI->generaltexth->allTexts[719]; boost::algorithm::replace_first(desc, "%s", ourCampaign->camp->scenarios[bonDescs[i].info2].scenarioName); //scenario } break; case CScenarioTravel::STravelBonus::HERO: desc = CGI->generaltexth->allTexts[718]; boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->capColors[bonDescs[i].info1]); //hero's color if (bonDescs[i].info2 == 0xFFFF) { boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->allTexts[101]); //hero's name picNumber = -1; picName = "CBONN1A3.BMP"; } else { boost::algorithm::replace_first(desc, "%s", CGI->heroh->heroes[bonDescs[i].info2]->name); //hero's name } break; } CToggleButton *bonusButton = new CToggleButton(Point(475 + i*68, 455), "", CButton::tooltip(desc, desc)); if (picNumber != -1) picName += ":" + boost::lexical_cast(picNumber); auto anim = new CAnimation(); anim->setCustom(picName, 0); bonusButton->setImage(anim); const SDL_Color brightYellow = { 242, 226, 110, 0 }; bonusButton->borderColor = brightYellow; bonuses->addToggle(i, bonusButton); } // set bonus if already chosen if(vstd::contains(ourCampaign->chosenCampaignBonuses, selectedMap)) { bonuses->setSelected(ourCampaign->chosenCampaignBonuses[selectedMap]); } } void CBonusSelection::updateCampaignState() { ourCampaign->currentMap = selectedMap; if (selectedBonus) ourCampaign->chosenCampaignBonuses[selectedMap] = *selectedBonus; } void CBonusSelection::startMap() { auto si = new StartInfo(startInfo); auto showPrologVideo = [=]() { auto exitCb = [=]() { logGlobal->infoStream() << "Starting scenario " << selectedMap; CGP->showLoadingScreen(std::bind(&startGame, si, (CConnection *)nullptr)); }; const CCampaignScenario & scenario = ourCampaign->camp->scenarios[selectedMap]; if (scenario.prolog.hasPrologEpilog) { GH.pushInt(new CPrologEpilogVideo(scenario.prolog, exitCb)); } else { exitCb(); } }; if(LOCPLINT) // we're currently ingame, so ask for starting new map and end game { GH.popInt(this); LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[67], [=] { updateCampaignState(); endGame(); GH.curInt = CGPreGame::create(); showPrologVideo(); }, 0); } else { updateCampaignState(); showPrologVideo(); } } void CBonusSelection::restartMap() { GH.popInt(this); LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[67], [=] { updateCampaignState(); auto si = new StartInfo(startInfo); SDL_Event event; event.type = SDL_USEREVENT; event.user.code = PREPARE_RESTART_CAMPAIGN; event.user.data1 = si; SDL_PushEvent(&event); }, 0); } void CBonusSelection::selectBonus(int id) { // Total redraw is needed because the border around the bonus images // have to be undrawn/drawn. if (!selectedBonus || *selectedBonus != id) { selectedBonus = id; GH.totalRedraw(); updateStartButtonState(id); } const CCampaignScenario &scenario = ourCampaign->camp->scenarios[selectedMap]; const std::vector & bonDescs = scenario.travelOptions.bonusesToChoose; if (bonDescs[id].type == CScenarioTravel::STravelBonus::HERO) { std::map names; names[1] = settings["general"]["playerName"].String(); for(auto & elem : startInfo.playerInfos) { if(elem.first == PlayerColor(bonDescs[id].info1)) ::setPlayer(elem.second, 1, names); else ::setPlayer(elem.second, 0, names); } } } void CBonusSelection::increaseDifficulty() { startInfo.difficulty = std::min(startInfo.difficulty + 1, 4); } void CBonusSelection::decreaseDifficulty() { startInfo.difficulty = std::max(startInfo.difficulty - 1, 0); } void CBonusSelection::updateStartButtonState(int selected /*= -1*/) { if(selected == -1) { startB->block(ourCampaign->camp->scenarios[selectedMap].travelOptions.bonusesToChoose.size()); } else if(startB->isBlocked()) { startB->block(false); } } CBonusSelection::CRegion::CRegion( CBonusSelection * _owner, bool _accessible, bool _selectable, int _myNumber ) : owner(_owner), accessible(_accessible), selectable(_selectable), myNumber(_myNumber) { OBJ_CONSTRUCTION; addUsedEvents(LCLICK | RCLICK); static const std::string colors[2][8] = { {"R", "B", "N", "G", "O", "V", "T", "P"}, {"Re", "Bl", "Br", "Gr", "Or", "Vi", "Te", "Pi"}}; const SCampPositions & campDsc = owner->campDescriptions[owner->ourCampaign->camp->header.mapVersion]; const SCampPositions::SRegionDesc & desc = campDsc.regions[myNumber]; pos.x += desc.xpos; pos.y += desc.ypos; //loading of graphics std::string prefix = campDsc.campPrefix + desc.infix + "_"; std::string suffix = colors[campDsc.colorSuffixLength - 1][owner->ourCampaign->camp->scenarios[myNumber].regionColor]; static const std::string infix [] = {"En", "Se", "Co"}; for (int g = 0; g < ARRAY_COUNT(infix); g++) { graphics[g] = BitmapHandler::loadBitmap(prefix + infix[g] + suffix + ".BMP"); } pos.w = graphics[0]->w; pos.h = graphics[0]->h; } CBonusSelection::CRegion::~CRegion() { for (auto & elem : graphics) { SDL_FreeSurface(elem); } } void CBonusSelection::CRegion::clickLeft( tribool down, bool previousState ) { //select if selectable & clicked inside our graphic if ( indeterminate(down) ) { return; } if( !down && selectable && !CSDL_Ext::isTransparent(graphics[0], GH.current->motion.x-pos.x, GH.current->motion.y-pos.y) ) { owner->selectMap(myNumber, false); owner->highlightedRegion = this; parent->showAll(screen); } } void CBonusSelection::CRegion::clickRight( tribool down, bool previousState ) { //show r-click text if( down && !CSDL_Ext::isTransparent(graphics[0], GH.current->motion.x-pos.x, GH.current->motion.y-pos.y) && rclickText.size() ) { CRClickPopup::createAndPush(rclickText); } } void CBonusSelection::CRegion::show(SDL_Surface * to) { //const SCampPositions::SRegionDesc & desc = owner->campDescriptions[owner->ourCampaign->camp->header.mapVersion].regions[myNumber]; if (!accessible) { //show as striped blitAtLoc(graphics[2], 0, 0, to); } else if (this == owner->highlightedRegion) { //show as selected blitAtLoc(graphics[1], 0, 0, to); } else { //show as not selected selected blitAtLoc(graphics[0], 0, 0, to); } } CSavingScreen::CSavingScreen(bool hotseat) : CSelectionScreen(CMenuScreen::saveGame, hotseat ? CMenuScreen::MULTI_HOT_SEAT : CMenuScreen::SINGLE_PLAYER) { ourGame = mapInfoFromGame(); sInfo = *LOCPLINT->cb->getStartInfo(); setPlayersFromGame(); } CSavingScreen::~CSavingScreen() { } ISelectionScreenInfo::ISelectionScreenInfo(const std::map *Names /*= nullptr*/) { multiPlayer = CMenuScreen::SINGLE_PLAYER; assert(!SEL); SEL = this; current = nullptr; if(Names && !Names->empty()) //if have custom set of player names - use it playerNames = *Names; else playerNames[1] = settings["general"]["playerName"].String(); //by default we have only one player and his name is "Player" (or whatever the last used name was) } ISelectionScreenInfo::~ISelectionScreenInfo() { assert(SEL == this); SEL = nullptr; } void ISelectionScreenInfo::updateStartInfo(std::string filename, StartInfo & sInfo, const CMapHeader * mapHeader) { ::updateStartInfo(filename, sInfo, mapHeader, playerNames); } void ISelectionScreenInfo::setPlayer(PlayerSettings &pset, ui8 player) { ::setPlayer(pset, player, playerNames); } ui8 ISelectionScreenInfo::getIdOfFirstUnallocatedPlayer() { for(auto i = playerNames.cbegin(); i != playerNames.cend(); i++) if(!sInfo.getPlayersSettings(i->first)) // return i->first; return 0; } bool ISelectionScreenInfo::isGuest() const { return multiPlayer == CMenuScreen::MULTI_NETWORK_GUEST; } bool ISelectionScreenInfo::isHost() const { return multiPlayer == CMenuScreen::MULTI_NETWORK_HOST; } void ChatMessage::apply(CSelectionScreen *selScreen) { selScreen->card->chat->addNewMessage(playerName + ": " + message); GH.totalRedraw(); } void QuitMenuWithoutStarting::apply(CSelectionScreen *selScreen) { if(!selScreen->ongoingClosing) { *selScreen->serv << this; //resend to confirm GH.popIntTotally(selScreen); //will wait with deleting us before this thread ends } vstd::clear_pointer(selScreen->serv); } void PlayerJoined::apply(CSelectionScreen *selScreen) { //assert(SEL->playerNames.size() == connectionID); //temporary, TODO when player exits SEL->playerNames[connectionID] = playerName; //put new player in first slot with AI for(auto & elem : SEL->sInfo.playerInfos) { if(!elem.second.playerID && !elem.second.compOnly) { selScreen->setPlayer(elem.second, connectionID); selScreen->opt->entries[elem.second.color]->selectButtons(); break; } } selScreen->propagateNames(); selScreen->propagateOptions(); selScreen->toggleTab(selScreen->curTab); GH.totalRedraw(); } void SelectMap::apply(CSelectionScreen *selScreen) { if(selScreen->isGuest()) { free = false; selScreen->changeSelection(mapInfo); } } void UpdateStartOptions::apply(CSelectionScreen *selScreen) { if(!selScreen->isGuest()) return; selScreen->setSInfo(*options); } void PregameGuiAction::apply(CSelectionScreen *selScreen) { if(!selScreen->isGuest()) return; switch(action) { case NO_TAB: selScreen->toggleTab(selScreen->curTab); break; case OPEN_OPTIONS: selScreen->toggleTab(selScreen->opt); break; case OPEN_SCENARIO_LIST: selScreen->toggleTab(selScreen->sel); break; case OPEN_RANDOM_MAP_OPTIONS: selScreen->toggleTab(selScreen->randMapTab); break; } } void RequestOptionsChange::apply(CSelectionScreen *selScreen) { if(!selScreen->isHost()) return; PlayerColor color = selScreen->sInfo.getPlayersSettings(playerID)->color; switch(what) { case TOWN: selScreen->opt->nextCastle(color, direction); break; case HERO: selScreen->opt->nextHero(color, direction); break; case BONUS: selScreen->opt->nextBonus(color, direction); break; } } void PlayerLeft::apply(CSelectionScreen *selScreen) { if(selScreen->isGuest()) return; SEL->playerNames.erase(playerID); if(PlayerSettings *s = selScreen->sInfo.getPlayersSettings(playerID)) //it's possible that player was unallocated { selScreen->setPlayer(*s, 0); selScreen->opt->entries[s->color]->selectButtons(); } selScreen->propagateNames(); selScreen->propagateOptions(); GH.totalRedraw(); } void PlayersNames::apply(CSelectionScreen *selScreen) { if(selScreen->isGuest()) selScreen->playerNames = playerNames; } void StartWithCurrentSettings::apply(CSelectionScreen *selScreen) { startingInfo.reset(); startingInfo.serv = selScreen->serv; startingInfo.sInfo = new StartInfo(selScreen->sInfo); if(!selScreen->ongoingClosing) { *selScreen->serv << this; //resend to confirm } selScreen->serv = nullptr; //hide it so it won't be deleted vstd::clear_pointer(selScreen->serverHandlingThread); //detach us saveGameName.clear(); CGP->showLoadingScreen(std::bind(&startGame, startingInfo.sInfo, startingInfo.serv)); throw 666; //EVIL, EVIL, EVIL workaround to kill thread (does "goto catch" outside listening loop) } CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode &config ) { pos.x += config["x"].Float(); pos.y += config["y"].Float(); pos.w = 200; pos.h = 116; campFile = config["file"].String(); video = config["video"].String(); OBJ_CONSTRUCTION_CAPTURING_ALL; status = config["open"].Bool() ? CCampaignScreen::ENABLED : CCampaignScreen::DISABLED; CCampaignHeader header = CCampaignHandler::getHeader(campFile); hoverText = header.name; if (status != CCampaignScreen::DISABLED) { addUsedEvents(LCLICK | HOVER); image = new CPicture(config["image"].String()); hoverLabel = new CLabel(pos.w / 2, pos.h + 20, FONT_MEDIUM, CENTER, Colors::YELLOW, ""); parent->addChild(hoverLabel); } if (status == CCampaignScreen::COMPLETED) checkMark = new CPicture("CAMPCHK"); } void CCampaignScreen::CCampaignButton::clickLeft(tribool down, bool previousState) { if (down) { // Close running video and open the selected campaign CCS->videoh->close(); GH.pushInt( new CBonusSelection(campFile) ); } } void CCampaignScreen::CCampaignButton::hover(bool on) { if (on) hoverLabel->setText(hoverText); // Shows the name of the campaign when you get into the bounds of the button else hoverLabel->setText(" "); } void CCampaignScreen::CCampaignButton::show(SDL_Surface * to) { if (status == CCampaignScreen::DISABLED) return; CIntObject::show(to); // Play the campaign button video when the mouse cursor is placed over the button if (hovered) { if (CCS->videoh->fname != video) CCS->videoh->open(video); CCS->videoh->update(pos.x, pos.y, to, true, false); // plays sequentially frame by frame, starts at the beginning when the video is over } else if (CCS->videoh->fname == video) // When you got out of the bounds of the button then close the video { CCS->videoh->close(); redraw(); } } CButton* CCampaignScreen::createExitButton(const JsonNode& button) { std::pair help; if (!button["help"].isNull() && button["help"].Float() > 0) help = CGI->generaltexth->zelp[button["help"].Float()]; std::function close = std::bind(&CGuiHandler::popIntTotally, &GH, this); return new CButton(Point(button["x"].Float(), button["y"].Float()), button["name"].String(), help, close, button["hotkey"].Float()); } CCampaignScreen::CCampaignScreen(const JsonNode &config) { OBJ_CONSTRUCTION_CAPTURING_ALL; for(const JsonNode& node : config["images"].Vector()) images.push_back(createPicture(node)); if (!images.empty()) { images[0]->center(); // move background to center moveTo(images[0]->pos.topLeft()); // move everything else to center images[0]->moveTo(pos.topLeft()); // restore moved twice background pos = images[0]->pos; // fix height\width of this window } if (!config["exitbutton"].isNull()) { back = createExitButton(config["exitbutton"]); back->hoverable = true; } for(const JsonNode& node : config["items"].Vector()) campButtons.push_back(new CCampaignButton(node)); } void CCampaignScreen::showAll(SDL_Surface *to) { CIntObject::showAll(to); if (pos.h != to->h || pos.w != to->w) CMessage::drawBorder(PlayerColor(1), to, pos.w+28, pos.h+30, pos.x-14, pos.y-15); } void CGPreGame::showLoadingScreen(std::function loader) { if (GH.listInt.size() && GH.listInt.front() == CGP) //pregame active CGP->removeFromGui(); GH.pushInt(new CLoadingScreen(loader)); } std::string CLoadingScreen::getBackground() { const auto & conf = CGPreGameConfig::get().getConfig()["loading"].Vector(); if(conf.empty()) { return "loadbar"; } else { return RandomGeneratorUtil::nextItem(conf, CRandomGenerator::getDefault())->String(); } } CLoadingScreen::CLoadingScreen(std::function loader): CWindowObject(BORDERED, getBackground()), loadingThread(loader) { CCS->musich->stopMusic(5000); } CLoadingScreen::~CLoadingScreen() { loadingThread.join(); } void CLoadingScreen::showAll(SDL_Surface *to) { Rect rect(0,0,to->w, to->h); SDL_FillRect(to, &rect, 0); CWindowObject::showAll(to); } CPrologEpilogVideo::CPrologEpilogVideo( CCampaignScenario::SScenarioPrologEpilog _spe, std::function callback ): CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), exitCb(callback) { OBJ_CONSTRUCTION_CAPTURING_ALL; addUsedEvents(LCLICK); pos = center(Rect(0,0, 800, 600)); updateShadow(); CCS->videoh->open(CCampaignHandler::prologVideoName(spe.prologVideo)); CCS->musich->playMusic("Music/" + CCampaignHandler::prologMusicName(spe.prologMusic), true); voiceSoundHandle = CCS->soundh->playSound(CCampaignHandler::prologVoiceName(spe.prologVideo)); text = new CMultiLineLabel(Rect(100, 500, 600, 100), EFonts::FONT_BIG, CENTER, Colors::METALLIC_GOLD, spe.prologText ); text->scrollTextTo(-100); } void CPrologEpilogVideo::show( SDL_Surface * to ) { CSDL_Ext::fillRectBlack(to, &pos); //BUG: some videos are 800x600 in size while some are 800x400 //VCMI should center them in the middle of the screen. Possible but needs modification //of video player API which I'd like to avoid until we'll get rid of Windows-specific player CCS->videoh->update(pos.x, pos.y, to, true, false); //move text every 5 calls/frames; seems to be good enough ++positionCounter; if(positionCounter % 5 == 0) { text->scrollTextBy(1); } else text->showAll(to);// blit text over video, if needed if (text->textSize.y + 100 < positionCounter / 5) clickLeft(false, false); } void CPrologEpilogVideo::clickLeft( tribool down, bool previousState ) { GH.popInt(this); CCS->soundh->stopSound(voiceSoundHandle); exitCb(); } CSimpleJoinScreen::CSimpleJoinScreen() { OBJ_CONSTRUCTION_CAPTURING_ALL; bg = new CPicture("MUDIALOG.bmp"); // address background pos = bg->center(); //center, window has size of bg graphic (x,y = 396,278 w=232 h=212) Rect boxRect(20, 20, 205, 50); title = new CTextBox("Enter address:", boxRect, 0, FONT_BIG, CENTER, Colors::WHITE); address = new CTextInput(Rect(25, 68, 175, 16), *bg); address->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); port = new CTextInput(Rect(25, 115, 175, 16), *bg); port->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); port->filters += std::bind(&CTextInput::numberFilter, _1, _2, 0, 65535); ok = new CButton(Point( 26, 142), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::enterSelectionScreen, this), SDLK_RETURN); cancel = new CButton(Point(142, 142), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CGuiHandler::popIntTotally, std::ref(GH), this), SDLK_ESCAPE); bar = new CGStatusBar(new CPicture(Rect(7, 186, 218, 18), 0)); port->setText(boost::lexical_cast(settings["server"]["port"].Float()), true); address->setText(settings["server"]["server"].String(), true); address->giveFocus(); } void CSimpleJoinScreen::enterSelectionScreen() { std::string textAddress = address->text; std::string textPort = port->text; GH.popIntTotally(this); GH.pushInt(new CSelectionScreen(CMenuScreen::newGame, CMenuScreen::MULTI_NETWORK_GUEST, nullptr, textAddress, textPort)); } void CSimpleJoinScreen::onChange(const std::string & newText) { ok->block(address->text.empty() || port->text.empty()); } vcmi-0.98/client/CPreGame.h000066400000000000000000000426241250671757600155120ustar00rootroot00000000000000#pragma once //#include "../lib/filesystem/Filesystem.h" #include "../lib/StartInfo.h" #include "../lib/FunctionList.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/rmg/CMapGenerator.h" #include "windows/CWindowObject.h" /* * CPreGame.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ class CMapInfo; class CMusicHandler; class CMapHeader; class CCampaignHeader; class CTextInput; class CCampaign; class CGStatusBar; class CTextBox; class CCampaignState; class CConnection; class JsonNode; class CMapGenOptions; class CRandomMapTab; struct CPackForSelectionScreen; struct PlayerInfo; class CMultiLineLabel; class CToggleButton; class CToggleGroup; class CTabbedInt; class CButton; class CSlider; namespace boost{ class thread; class recursive_mutex;} enum ESortBy{_playerAm, _size, _format, _name, _viccon, _loscon, _numOfMaps, _fileName}; //_numOfMaps is for campaigns /// Class which handles map sorting by different criteria class mapSorter { public: ESortBy sortBy; bool operator()(const CMapInfo *aaa, const CMapInfo *bbb); mapSorter(ESortBy es):sortBy(es){}; }; /// The main menu screens listed in the EState enum class CMenuScreen : public CIntObject { const JsonNode& config; CTabbedInt *tabs; CPicture * background; std::vector images; CIntObject *createTab(size_t index); public: std::vector menuNameToEntry; enum EState { //where are we? mainMenu, newGame, loadGame, campaignMain, saveGame, scenarioInfo, campaignList }; enum EMultiMode { SINGLE_PLAYER = 0, MULTI_HOT_SEAT, MULTI_NETWORK_HOST, MULTI_NETWORK_GUEST }; CMenuScreen(const JsonNode& configNode); void showAll(SDL_Surface * to); void show(SDL_Surface * to); void activate(); void deactivate(); void switchToTab(size_t index); }; class CMenuEntry : public CIntObject { std::vector images; std::vector buttons; CButton* createButton(CMenuScreen* parent, const JsonNode& button); public: CMenuEntry(CMenuScreen* parent, const JsonNode &config); }; class CreditsScreen : public CIntObject { int positionCounter; CMultiLineLabel* credits; public: CreditsScreen(); void show(SDL_Surface * to); void clickLeft(tribool down, bool previousState); void clickRight(tribool down, bool previousState); }; /// Implementation of the chat box class CChatBox : public CIntObject { public: CTextBox *chatHistory; CTextInput *inputBox; CChatBox(const Rect &rect); void keyPressed(const SDL_KeyboardEvent & key); void addNewMessage(const std::string &text); }; class InfoCard : public CIntObject { public: CPicture *bg; CMenuScreen::EState type; bool network; bool chatOn; //if chat is shown, then description is hidden CTextBox *mapDescription; CChatBox *chat; CPicture *playerListBg; CToggleGroup *difficulty; CDefHandler *sizes, *sFlags; void changeSelection(const CMapInfo *to); void showAll(SDL_Surface * to); void clickRight(tribool down, bool previousState); void showTeamsPopup(); void toggleChat(); void setChat(bool activateChat); InfoCard(bool Network = false); ~InfoCard(); }; /// The selection tab which is shown at the map selection screen class SelectionTab : public CIntObject { private: CDefHandler *format; //map size void parseMaps(const std::unordered_set &files); void parseGames(const std::unordered_set &files, bool multi); void parseCampaigns(const std::unordered_set & files ); std::unordered_set getFiles(std::string dirURI, int resType); CMenuScreen::EState tabType; public: int positions; //how many entries (games/maps) can be shown CPicture *bg; //general bg image CSlider *slider; std::vector allItems; std::vector curItems; size_t selectionPos; std::function onSelect; ESortBy sortingBy; ESortBy generalSortingBy; bool ascending; CTextInput *txt; void filter(int size, bool selectFirst = false); //0 - all void select(int position); //position: <0 - positions> position on the screen void selectAbs(int position); //position: absolute position in curItems vector int getPosition(int x, int y); //convert mouse coords to entry position; -1 means none void sliderMove(int slidPos); void sortBy(int criteria); void sort(); void printMaps(SDL_Surface *to); int getLine(); void selectFName(std::string fname); const CMapInfo * getSelectedMapInfo() const; void showAll(SDL_Surface * to); void clickLeft(tribool down, bool previousState); void keyPressed(const SDL_KeyboardEvent & key); void onDoubleClick(); SelectionTab(CMenuScreen::EState Type, const std::function &OnSelect, CMenuScreen::EMultiMode MultiPlayer = CMenuScreen::SINGLE_PLAYER); ~SelectionTab(); }; /// The options tab which is shown at the map selection phase. class OptionsTab : public CIntObject { CPicture *bg; public: enum SelType {TOWN, HERO, BONUS}; struct CPlayerSettingsHelper { const PlayerSettings & settings; const SelType type; CPlayerSettingsHelper(const PlayerSettings & settings, SelType type): settings(settings), type(type) {} /// visible image settings size_t getImageIndex(); std::string getImageName(); std::string getName(); /// name visible in options dialog std::string getTitle(); /// title in popup box std::string getSubtitle(); /// popup box subtitle std::string getDescription();/// popup box description, not always present }; class CPregameTooltipBox : public CWindowObject, public CPlayerSettingsHelper { void genHeader(); void genTownWindow(); void genHeroWindow(); void genBonusWindow(); public: CPregameTooltipBox(CPlayerSettingsHelper & helper); }; struct SelectedBox : public CIntObject, public CPlayerSettingsHelper //img with current town/hero/bonus { CAnimImage * image; CLabel *subtitle; SelectedBox(Point position, PlayerSettings & settings, SelType type); void clickRight(tribool down, bool previousState); void update(); }; struct PlayerOptionsEntry : public CIntObject { PlayerInfo π PlayerSettings &s; CPicture *bg; CButton *btns[6]; //left and right for town, hero, bonus CButton *flag; SelectedBox *town; SelectedBox *hero; SelectedBox *bonus; enum {HUMAN_OR_CPU, HUMAN, CPU} whoCanPlay; PlayerOptionsEntry(OptionsTab *owner, PlayerSettings &S); void selectButtons(); //hides unavailable buttons void showAll(SDL_Surface * to); void update(); }; CSlider *turnDuration; std::set usedHeroes; struct PlayerToRestore { PlayerColor color; int id; void reset() { id = -1; color = PlayerColor::CANNOT_DETERMINE; } PlayerToRestore(){ reset(); } } playerToRestore; std::map entries; //indexed by color void nextCastle(PlayerColor player, int dir); //dir == -1 or +1 void nextHero(PlayerColor player, int dir); //dir == -1 or +1 void nextBonus(PlayerColor player, int dir); //dir == -1 or +1 void setTurnLength(int npos); void flagPressed(PlayerColor player); void recreate(); OptionsTab(); ~OptionsTab(); void showAll(SDL_Surface * to); int nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir ); bool canUseThisHero(PlayerColor player, int ID ); }; /// The random map tab shows options for generating a random map. class CRandomMapTab : public CIntObject { public: CRandomMapTab(); void showAll(SDL_Surface * to); void updateMapInfo(); CFunctionList & getMapInfoChanged(); const CMapInfo * getMapInfo() const; const CMapGenOptions & getMapGenOptions() const; void setMapGenOptions(shared_ptr opts); private: void addButtonsToGroup(CToggleGroup * group, const std::vector & defs, int startIndex, int endIndex, int btnWidth, int helpStartIndex) const; void addButtonsWithRandToGroup(CToggleGroup * group, const std::vector & defs, int startIndex, int endIndex, int btnWidth, int helpStartIndex, int helpRandIndex) const; void deactivateButtonsFrom(CToggleGroup * group, int startId); void validatePlayersCnt(int playersCnt); void validateCompOnlyPlayersCnt(int compOnlyPlayersCnt); std::vector getPossibleMapSizes(); CPicture * bg; CToggleButton * twoLevelsBtn; CToggleGroup * mapSizeBtnGroup, * playersCntGroup, * teamsCntGroup, * compOnlyPlayersCntGroup, * compOnlyTeamsCntGroup, * waterContentGroup, * monsterStrengthGroup; CButton * showRandMaps; CMapGenOptions mapGenOptions; unique_ptr mapInfo; CFunctionList mapInfoChanged; }; /// Interface for selecting a map. class ISelectionScreenInfo { public: CMenuScreen::EMultiMode multiPlayer; CMenuScreen::EState screenType; //new/save/load#Game const CMapInfo *current; StartInfo sInfo; std::map playerNames; // id of player <-> player name; 0 is reserved as ID of AI "players" ISelectionScreenInfo(const std::map *Names = nullptr); virtual ~ISelectionScreenInfo(); virtual void update(){}; virtual void propagateOptions() {}; virtual void postRequest(ui8 what, ui8 dir) {}; virtual void postChatMessage(const std::string &txt){}; void setPlayer(PlayerSettings &pset, ui8 player); void updateStartInfo( std::string filename, StartInfo & sInfo, const CMapHeader * mapHeader ); ui8 getIdOfFirstUnallocatedPlayer(); //returns 0 if none bool isGuest() const; bool isHost() const; }; /// The actual map selection screen which consists of the options and selection tab class CSelectionScreen : public CIntObject, public ISelectionScreenInfo { bool bordered; public: CPicture *bg; //general bg image InfoCard *card; OptionsTab *opt; CRandomMapTab * randMapTab; CButton *start, *back; SelectionTab *sel; CIntObject *curTab; boost::thread *serverHandlingThread; boost::recursive_mutex *mx; std::list upcomingPacks; //protected by mx CConnection *serv; //connection to server, used in MP mode bool ongoingClosing; ui8 myNameID; //used when networking - otherwise all player are "mine" CSelectionScreen(CMenuScreen::EState Type, CMenuScreen::EMultiMode MultiPlayer = CMenuScreen::SINGLE_PLAYER, const std::map * Names = nullptr, const std::string & Address = "", const std::string & Port = ""); ~CSelectionScreen(); void toggleTab(CIntObject *tab); void changeSelection(const CMapInfo *to); void startCampaign(); void startScenario(); void difficultyChange(int to); void handleConnection(); void processPacks(); void setSInfo(const StartInfo &si); void update() override; void propagateOptions() override; void postRequest(ui8 what, ui8 dir) override; void postChatMessage(const std::string &txt) override; void propagateNames(); void showAll(SDL_Surface *to); }; /// Save game screen class CSavingScreen : public CSelectionScreen { public: const CMapInfo *ourGame; CSavingScreen(bool hotseat = false); ~CSavingScreen(); }; /// Scenario information screen shown during the game (thus not really a "pre-game" but fits here anyway) class CScenarioInfo : public CIntObject, public ISelectionScreenInfo { public: CButton *back; InfoCard *card; OptionsTab *opt; CScenarioInfo(const CMapHeader *mapHeader, const StartInfo *startInfo); ~CScenarioInfo(); }; /// Multiplayer mode class CMultiMode : public CIntObject { public: CPicture *bg; CTextInput *txt; CButton *btns[7]; //0 - hotseat, 6 - cancel CGStatusBar *bar; CMultiMode(); void openHotseat(); void hostTCP(); void joinTCP(); }; /// Hot seat player window class CHotSeatPlayers : public CIntObject { CPicture *bg; CTextBox *title; CTextInput* txt[8]; CButton *ok, *cancel; CGStatusBar *bar; void onChange(std::string newText); void enterSelectionScreen(); public: CHotSeatPlayers(const std::string &firstPlayer); }; class CPrologEpilogVideo : public CWindowObject { CCampaignScenario::SScenarioPrologEpilog spe; int positionCounter; int voiceSoundHandle; std::function exitCb; CMultiLineLabel * text; public: CPrologEpilogVideo(CCampaignScenario::SScenarioPrologEpilog _spe, std::function callback); void clickLeft(tribool down, bool previousState); void show(SDL_Surface * to); }; /// Campaign screen where you can choose one out of three starting bonuses class CBonusSelection : public CIntObject { public: CBonusSelection(const std::string & campaignFName); CBonusSelection(shared_ptr _ourCampaign); ~CBonusSelection(); void showAll(SDL_Surface * to) override; void show(SDL_Surface * to) override; private: struct SCampPositions { std::string campPrefix; int colorSuffixLength; struct SRegionDesc { std::string infix; int xpos, ypos; }; std::vector regions; }; class CRegion : public CIntObject { CBonusSelection * owner; SDL_Surface* graphics[3]; //[0] - not selected, [1] - selected, [2] - striped bool accessible; //false if region should be striped bool selectable; //true if region should be selectable int myNumber; //number of region public: std::string rclickText; CRegion(CBonusSelection * _owner, bool _accessible, bool _selectable, int _myNumber); ~CRegion(); void clickLeft(tribool down, bool previousState); void clickRight(tribool down, bool previousState); void show(SDL_Surface * to); }; void init(); void loadPositionsOfGraphics(); void updateStartButtonState(int selected = -1); //-1 -- no bonus is selected void updateBonusSelection(); void updateCampaignState(); // Event handlers void goBack(); void startMap(); void restartMap(); void selectMap(int mapNr, bool initialSelect); void selectBonus(int id); void increaseDifficulty(); void decreaseDifficulty(); // GUI components SDL_Surface * background; CButton * startB, * restartB, * backB; CTextBox * campaignDescription, * mapDescription; std::vector campDescriptions; std::vector regions; CRegion * highlightedRegion; CToggleGroup * bonuses; SDL_Surface * diffPics[5]; //pictures of difficulties, user-selectable (or not if campaign locks this) CButton * diffLb, * diffRb; //buttons for changing difficulty CDefHandler * sizes; //icons of map sizes CDefHandler * sFlags; // Data shared_ptr ourCampaign; int selectedMap; boost::optional selectedBonus; StartInfo startInfo; CMapHeader * ourHeader; }; /// Campaign selection screen class CCampaignScreen : public CIntObject { public: enum CampaignStatus {DEFAULT = 0, ENABLED, DISABLED, COMPLETED}; // the status of the campaign private: /// A button which plays a video when you move the mouse cursor over it class CCampaignButton : public CIntObject { private: CPicture *image; CPicture *checkMark; CLabel *hoverLabel; CampaignStatus status; std::string campFile; // the filename/resourcename of the campaign std::string video; // the resource name of the video std::string hoverText; void clickLeft(tribool down, bool previousState); void hover(bool on); public: CCampaignButton(const JsonNode &config ); void show(SDL_Surface * to); }; CButton *back; std::vector campButtons; std::vector images; CButton* createExitButton(const JsonNode& button); public: enum CampaignSet {ROE, AB, SOD, WOG}; CCampaignScreen(const JsonNode &config); void showAll(SDL_Surface *to); }; /// Manages the configuration of pregame GUI elements like campaign screen, main menu, loading screen,... class CGPreGameConfig { public: static CGPreGameConfig & get(); const JsonNode & getConfig() const; const JsonNode & getCampaigns() const; private: CGPreGameConfig(); const JsonNode campaignSets; const JsonNode config; }; /// Handles background screen, loads graphics for victory/loss condition and random town or hero selection class CGPreGame : public CIntObject, public ILockedUpdatable { void loadGraphics(); void disposeGraphics(); CGPreGame(); //Use createIfNotPresent public: CMenuScreen * menu; CDefHandler *victory, *loss; ~CGPreGame(); void update() override; void runLocked(std::function cb) override; void openSel(CMenuScreen::EState type, CMenuScreen::EMultiMode multi = CMenuScreen::SINGLE_PLAYER); void openCampaignScreen(std::string name); static CGPreGame * create(); void removeFromGui(); static void showLoadingScreen(std::function loader); }; class CLoadingScreen : public CWindowObject { boost::thread loadingThread; std::string getBackground(); public: CLoadingScreen(std::function loader); ~CLoadingScreen(); void showAll(SDL_Surface *to); }; /// Simple window to enter the server's address. class CSimpleJoinScreen : public CIntObject { CPicture * bg; CTextBox * title; CButton * ok, * cancel; CGStatusBar * bar; CTextInput * address; CTextInput * port; void enterSelectionScreen(); void onChange(const std::string & newText); public: CSimpleJoinScreen(); }; extern ISelectionScreenInfo *SEL; extern CGPreGame *CGP; vcmi-0.98/client/CVideoHandler.cpp000066400000000000000000000245231250671757600170670ustar00rootroot00000000000000#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() { format = nullptr; frame = nullptr; codec = nullptr; sws = nullptr; #ifdef VCMI_SDL1 overlay = nullptr; #else texture = nullptr; #endif dest = nullptr; context = nullptr; // 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/* = false*/) { 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 /*= false*/) { close(); this->fname = fname; refreshWait = 3; refreshCount = -1; doLoop = loop; ResourceID resource(std::string("Video/") + fname, EResType::VIDEO); if (!CResourceHandler::get()->existsResource(resource)) { logGlobal->errorStream() << "Error: video " << resource.getName() << " was not found"; 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 LIBAVCODEC_VERSION_INT < AV_VERSION_INT(53, 17, 0) if (av_find_stream_info(format) < 0) #else if (avformat_find_stream_info(format, nullptr) < 0) #endif 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 LIBAVCODEC_VERSION_INT < AV_VERSION_INT(53, 6, 0) if ( avcodec_open(codecContext, codec) < 0 ) #else if ( avcodec_open2(codecContext, codec, nullptr) < 0 ) #endif { // Could not open codec codec = nullptr; return false; } // Allocate video frame frame = avcodec_alloc_frame(); //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) { #ifdef VCMI_SDL1 overlay = SDL_CreateYUVOverlay(pos.w, pos.h, SDL_YV12_OVERLAY, screen); #else texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h); #endif } else { dest = CSDL_Ext::newSurface(pos.w, pos.h); destRect.x = destRect.y = 0; destRect.w = pos.w; destRect.h = pos.h; } #ifdef VCMI_SDL1 if (overlay == nullptr && dest == nullptr) return false; if (overlay) #else if (texture == nullptr && dest == nullptr) return false; if (texture) #endif { // Convert the image into YUV format that SDL uses sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, pos.w, pos.h, PIX_FMT_YUV420P, SWS_BICUBIC, nullptr, nullptr, nullptr); } else { PixelFormat screenFormat = PIX_FMT_NONE; if (screen->format->Bshift > screen->format->Rshift) { // this a BGR surface switch (screen->format->BytesPerPixel) { case 2: screenFormat = PIX_FMT_BGR565; break; case 3: screenFormat = PIX_FMT_BGR24; break; case 4: screenFormat = PIX_FMT_BGR32; break; default: return false; } } else { // this a RGB surface switch (screen->format->BytesPerPixel) { case 2: screenFormat = PIX_FMT_RGB565; break; case 3: screenFormat = PIX_FMT_RGB24; break; case 4: screenFormat = 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; #ifdef VCMI_SDL1 if (overlay) { SDL_LockYUVOverlay(overlay); pict.data[0] = overlay->pixels[0]; pict.data[1] = overlay->pixels[2]; pict.data[2] = overlay->pixels[1]; pict.linesize[0] = overlay->pitches[0]; pict.linesize[1] = overlay->pitches[2]; pict.linesize[2] = overlay->pitches[1]; sws_scale(sws, frame->data, frame->linesize, 0, codecContext->height, pict.data, pict.linesize); SDL_UnlockYUVOverlay(overlay); #else 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); #endif } 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; } #ifdef VCMI_SDL1 if (overlay) { SDL_FreeYUVOverlay(overlay); overlay = nullptr; } #else if (texture) { SDL_DestroyTexture(texture); texture = nullptr; } #endif if (dest) { SDL_FreeSurface(dest); dest = nullptr; } if (frame) { av_free(frame); frame = nullptr; } if (codec) { avcodec_close(codecContext); codec = nullptr; codecContext = nullptr; } if (format) { #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(53, 17, 0) av_close_input_file(format); format = nullptr; #else avformat_close_input(&format); #endif } if (context) { av_free(context); context = nullptr; } } // Plays a video. Only works for overlays. bool CVideoPlayer::playVideo(int x, int y, SDL_Surface *dst, 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; #ifdef VCMI_SDL1 SDL_DisplayYUVOverlay(overlay, &pos); #else SDL_RenderCopy(mainRenderer, texture, NULL, &pos); SDL_RenderPresent(mainRenderer); #endif // Wait 3 frames GH.mainFPSmng->framerateDelay(); GH.mainFPSmng->framerateDelay(); GH.mainFPSmng->framerateDelay(); } return true; } bool CVideoPlayer::openAndPlayVideo(std::string name, int x, int y, SDL_Surface *dst, bool stopOnKey, bool scale/* = false*/) { open(name, false, true, scale); bool ret = playVideo(x, y, dst, stopOnKey); close(); return ret; } CVideoPlayer::~CVideoPlayer() { close(); } #endif vcmi-0.98/client/CVideoHandler.h000066400000000000000000000076071250671757600165400ustar00rootroot00000000000000#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, SDL_Surface *dst, 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 #if SDL_VERSION_ATLEAST(1,3,0) && !SDL_VERSION_ATLEAST(2,0,0) #include #endif extern "C" { #include #include // compatibility with different versions od libavutil #if (LIBAVUTIL_VERSION_INT < AV_VERSION_INT(51, 42, 0)) || \ (LIBAVUTIL_VERSION_INT == AV_VERSION_INT(51, 73, 101)) #define AV_PIX_FMT_NONE PIX_FMT_NONE #define AV_PIX_FMT_NV12 PIX_FMT_NV12 #define AV_PIX_FMT_YUV420P PIX_FMT_YUV420P #define AV_PIX_FMT_UYVY422 PIX_FMT_UYVY422 #define AV_PIX_FMT_YUYV422 PIX_FMT_YUYV422 #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. #ifdef VCMI_SDL1 SDL_Overlay * overlay; #else SDL_Texture *texture; #endif 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, SDL_Surface *dst, 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, SDL_Surface *dst, 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.98/client/Client.cpp000066400000000000000000000710341250671757600156350ustar00rootroot00000000000000#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/BattleState.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/Connection.h" #ifndef VCMI_ANDROID #include "../lib/Interprocess.h" #endif #include "../lib/NetPacks.h" #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" #include "../lib/mapping/CMap.h" #include "../lib/JsonNode.h" #include "mapHandler.h" #include "../lib/CConfigHandler.h" #include "Client.h" #include "CPreGame.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" extern std::string NAME; #ifndef VCMI_ANDROID namespace intpr = boost::interprocess; #endif /* * Client.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ 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 { T *ptr = static_cast(pack); ptr->applyCl(cl); } void applyOnClBefore(CClient *cl, void *pack) const { T *ptr = static_cast(pack); ptr->applyFirstCl(cl); } }; template <> class CApplyOnCL : public CBaseForCLApply { public: void applyOnClAfter(CClient *cl, void *pack) const { logGlobal->errorStream() << "Cannot apply on CL plain CPack!"; assert(0); } void applyOnClBefore(CClient *cl, void *pack) const { logGlobal->errorStream() << "Cannot apply on CL plain CPack!"; assert(0); } }; static CApplier *applier = nullptr; void CClient::init() { hotSeat = false; connectionHandler = nullptr; pathInfo = nullptr; applier = new CApplier; registerTypesClientPacks1(*applier); registerTypesClientPacks2(*applier); IObjectInterface::cb = this; serv = nullptr; gs = nullptr; erm = nullptr; terminate = false; } CClient::CClient(void) { init(); } CClient::CClient(CConnection *con, StartInfo *si) { init(); newGame(con,si); } CClient::~CClient(void) { delete applier; } 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)); logNetwork->traceStream() << "Send battle action to server: " << ba; MakeAction temp_action(ba); sendRequest(&temp_action, color); return; } catch(boost::thread_interrupted&) { logNetwork->debugStream() << "Wait for move thread was interrupted and no action will be send. Was a battle ended by spell?"; return; } catch(...) { handleException(); return; } logNetwork->errorStream() << "We should not be here!"; } void CClient::run() { setThreadName("CClient::run"); try { while(!terminate) { CPack *pack = serv->retreivePack(); //get the package from the server if (terminate) { vstd::clear_pointer(pack); break; } handlePack(pack); } } //catch only asio exceptions catch (const boost::system::system_error& e) { logNetwork->errorStream() << "Lost connection to server, ending listening thread!"; logNetwork->errorStream() << e.what(); if(!terminate) //rethrow (-> boom!) only if closing connection was unexpected { logNetwork->errorStream() << "Something wrong, lost connection while game is still ongoing..."; throw; } } } void CClient::save(const std::string & fname) { if(gs->curB) { logNetwork->errorStream() << "Game cannot be saved during battle!"; return; } SaveGame save_game(fname); sendRequest((CPackForClient*)&save_game, PlayerColor::NEUTRAL); } void CClient::endGame( bool closeConnection /*= true*/ ) { //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) for(auto i : playerint) i.second->finish(); // Game is ending // Tell the network thread to reach a stable state if(closeConnection) stopConnection(); logNetwork->infoStream() << "Closed connection."; GH.curInt = nullptr; { boost::unique_lock un(*LOCPLINT->pim); logNetwork->infoStream() << "Ending current game!"; if(GH.topInt()) GH.topInt()->deactivate(); GH.listInt.clear(); GH.objsToBlit.clear(); GH.statusbar = nullptr; logNetwork->infoStream() << "Removed GUI."; vstd::clear_pointer(const_cast(CGI)->mh); vstd::clear_pointer(gs); logNetwork->infoStream() << "Deleted mapHandler and gameState."; LOCPLINT = nullptr; } playerint.clear(); battleints.clear(); callbacks.clear(); battleCallbacks.clear(); logNetwork->infoStream() << "Deleted playerInts."; logNetwork->infoStream() << "Client stopped."; } #if 1 void CClient::loadGame(const std::string & fname, const bool server, const std::vector& humanplayerindices, const int loadNumPlayers, int player_, const std::string & ipaddr, const std::string & port) { PlayerColor player(player_); //intentional shadowing logNetwork->infoStream() <<"Loading procedure started!"; CServerHandler sh; if(server) sh.startServer(); else serv = sh.justConnectToServer(ipaddr,port=="" ? "3030" : port); CStopWatch tmh; unique_ptr loader; try { std::string clientSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(fname, EResType::CLIENT_SAVEGAME)); std::string controlServerSaveName; if (CResourceHandler::get("local")->existsResource(ResourceID(fname, EResType::SERVER_SAVEGAME))) { controlServerSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(fname, EResType::SERVER_SAVEGAME)); } else// create entry for server savegame. Triggered if save was made after launch and not yet present in res handler { controlServerSaveName = clientSaveName.substr(0, clientSaveName.find_last_of(".")) + ".vsgm1"; CResourceHandler::get("local")->createResource(controlServerSaveName, true); } if(clientSaveName.empty()) throw std::runtime_error("Cannot open client part of " + fname); if(controlServerSaveName.empty()) throw std::runtime_error("Cannot open server part of " + fname); { CLoadIntegrityValidator checkingLoader(clientSaveName, controlServerSaveName, minSupportedVersion); loadCommonState(checkingLoader); loader = checkingLoader.decay(); } logNetwork->infoStream() << "Loaded common part of save " << tmh.getDiff(); const_cast(CGI)->mh = new CMapHandler(); const_cast(CGI)->mh->map = gs->map; pathInfo = make_unique(getMapSize()); CGI->mh->init(); logNetwork->infoStream() <<"Initing maphandler: "<errorStream() << "Cannot load game " << fname << ". Error: " << e.what(); throw; //obviously we cannot continue here } /* if(!server) player = PlayerColor(player_); */ std::set clientPlayers; if(server) serv = sh.connectToServer(); //*loader >> *this; if(server) { tmh.update(); ui8 pom8; *serv << ui8(3) << ui8(loadNumPlayers); //load game; one client if single-player *serv << fname; *serv >> pom8; if(pom8) throw std::runtime_error("Server cannot open the savegame!"); else logNetwork->infoStream() << "Server opened savegame properly."; } if(server) { for(auto & elem : gs->scenarioOps->playerInfos) if(!std::count(humanplayerindices.begin(),humanplayerindices.end(),elem.first.getNum()) || elem.first==player) { clientPlayers.insert(elem.first); } clientPlayers.insert(PlayerColor::NEUTRAL); } else { clientPlayers.insert(player); } std::cout << "CLIENTPLAYERS:\n"; for(auto x : clientPlayers) std::cout << x << std::endl; std::cout << "ENDCLIENTPLAYERS\n"; serialize(loader->serializer,0,clientPlayers); *serv << ui32(clientPlayers.size()); for(auto & elem : clientPlayers) *serv << ui8(elem.getNum()); serv->addStdVecItems(gs); /*why is this here?*/ //*loader >> *this; logNetwork->infoStream() << "Loaded client part of save " << tmh.getDiff(); logNetwork->infoStream() <<"Sent info to server: "<enableStackSendingByID(); serv->disableSmartPointerSerialization(); // logGlobal->traceStream() << "Objects:"; // for(int i = 0; i < gs->map->objects.size(); i++) // { // auto o = gs->map->objects[i]; // if(o) // logGlobal->traceStream() << boost::format("\tindex=%5d, id=%5d; address=%5d, pos=%s, name=%s") % i % o->id % (int)o.get() % o->pos % o->getHoverText(); // else // logGlobal->traceStream() << boost::format("\tindex=%5d --- nullptr") % i; // } } #endif void CClient::newGame( CConnection *con, StartInfo *si ) { enum {SINGLE, HOST, GUEST} networkMode = SINGLE; if (con == nullptr) { CServerHandler sh; serv = sh.connectToServer(); } else { serv = con; networkMode = (con->connectionID == 1) ? HOST : GUEST; } CConnection &c = *serv; //////////////////////////////////////////////////// logNetwork->infoStream() <<"\tWill send info to server..."; CStopWatch tmh; if(networkMode == SINGLE) { ui8 pom8; c << ui8(2) << ui8(1); //new game; one client c << *si; c >> pom8; if(pom8) throw std::runtime_error("Server cannot open the map!"); } c >> si; logNetwork->infoStream() <<"\tSending/Getting info to/from the server: "<infoStream() <<"\tCreating gamestate: "<scenarioOps = si; gs->init(si); logNetwork->infoStream() <<"Initializing GameState (together): "< myPlayers; for(auto & elem : gs->scenarioOps->playerInfos) { if((networkMode == SINGLE) //single - one client has all player || (networkMode != SINGLE && serv->connectionID == elem.second.playerID) //multi - client has only "its players" || (networkMode == HOST && elem.second.playerID == PlayerSettings::PLAYER_AI))//multi - host has all AI players { myPlayers.insert(elem.first); //add player } } if(networkMode != GUEST) myPlayers.insert(PlayerColor::NEUTRAL); c << myPlayers; // Init map handler if(gs->map) { const_cast(CGI)->mh = new CMapHandler(); CGI->mh->map = gs->map; logNetwork->infoStream() <<"Creating mapHandler: "<mh->init(); pathInfo = make_unique(getMapSize()); logNetwork->infoStream() <<"Initializing mapHandler (together): "<scenarioOps->playerInfos)//initializing interfaces for players { PlayerColor color = elem.first; gs->currentPlayer = color; if(!vstd::contains(myPlayers, color)) continue; logNetwork->traceStream() << "Preparing interface for player " << color; if(si->mode != StartInfo::DUEL) { if(elem.second.playerID == PlayerSettings::PLAYER_AI) { auto AiToGive = aiNameForPlayer(elem.second, false); logNetwork->infoStream() << boost::format("Player %s will be lead by %s") % color % AiToGive; installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); } else { installNewPlayerInterface(make_shared(color), color); humanPlayers++; } } else { std::string AItoGive = aiNameForPlayer(elem.second, true); installNewBattleInterface(CDynLibHandler::getNewBattleAI(AItoGive), color); } } if(si->mode == StartInfo::DUEL) { if(!gNoGUI) { boost::unique_lock un(*LOCPLINT->pim); auto p = make_shared(PlayerColor::NEUTRAL); p->observerInDuelMode = true; installNewPlayerInterface(p, boost::none); GH.curInt = p.get(); } battleStarted(gs->curB); } else { loadNeutralBattleAI(); } serv->addStdVecItems(gs); hotSeat = (humanPlayers > 1); // std::vector scriptModules; // CFileUtility::getFilesWithExt(scriptModules, LIB_DIR "/scripting", "." LIB_EXT); // for(FileInfo &m : scriptModules) // { // CScriptingModule * nm = CDynLibHandler::getNewScriptingModule(m.name); // privilagedGameEventReceivers.push_back(nm); // privilagedBattleEventReceivers.push_back(nm); // nm->giveActionCB(this); // nm->giveInfoCB(this); // nm->init(); // // erm = nm; //something tells me that there'll at most one module and it'll be ERM // } } void CClient::serialize(COSer & h, const int version) { assert(h.saving); h & hotSeat; { ui8 players = playerint.size(); h & players; for(auto i = playerint.begin(); i != playerint.end(); i++) { LOG_TRACE_PARAMS(logGlobal, "Saving player %s interface", i->first); assert(i->first == i->second->playerID); h & i->first & i->second->dllName & i->second->human; i->second->saveGame(h, version); } } } void CClient::serialize(CISer & h, const int version) { assert(!h.saving); h & hotSeat; { ui8 players = 0; //fix for uninitialized warning h & players; for(int i=0; i < players; i++) { std::string dllname; PlayerColor pid; bool isHuman = false; h & pid & dllname & isHuman; LOG_TRACE_PARAMS(logGlobal, "Loading player %s interface", pid); shared_ptr nInt; if(dllname.length()) { if(pid == PlayerColor::NEUTRAL) { installNewBattleInterface(CDynLibHandler::getNewBattleAI(dllname), pid); //TODO? consider serialization continue; } else { assert(!isHuman); nInt = CDynLibHandler::getNewAI(dllname); } } else { assert(isHuman); nInt = make_shared(pid); } nInt->dllName = dllname; nInt->human = isHuman; nInt->playerID = pid; installNewPlayerInterface(nInt, pid); nInt->loadGame(h, version); //another evil cast, check above } if(!vstd::contains(battleints, PlayerColor::NEUTRAL)) loadNeutralBattleAI(); } } void CClient::serialize(COSer & h, const int version, const std::set & playerIDs) { assert(h.saving); h & hotSeat; { ui8 players = playerint.size(); h & players; for(auto i = playerint.begin(); i != playerint.end(); i++) { LOG_TRACE_PARAMS(logGlobal, "Saving player %s interface", i->first); assert(i->first == i->second->playerID); h & i->first & i->second->dllName & i->second->human; i->second->saveGame(h, version); } } } void CClient::serialize(CISer & h, const int version, const std::set & playerIDs) { assert(!h.saving); h & hotSeat; { ui8 players = 0; //fix for uninitialized warning h & players; for(int i=0; i < players; i++) { std::string dllname; PlayerColor pid; bool isHuman = false; h & pid & dllname & isHuman; LOG_TRACE_PARAMS(logGlobal, "Loading player %s interface", pid); shared_ptr nInt; if(dllname.length()) { if(pid == PlayerColor::NEUTRAL) { if(playerIDs.count(pid)) installNewBattleInterface(CDynLibHandler::getNewBattleAI(dllname), pid); //TODO? consider serialization continue; } else { assert(!isHuman); nInt = CDynLibHandler::getNewAI(dllname); } } else { assert(isHuman); nInt = make_shared(pid); } nInt->dllName = dllname; nInt->human = isHuman; nInt->playerID = pid; if(playerIDs.count(pid)) installNewPlayerInterface(nInt, pid); nInt->loadGame(h, version); } if(playerIDs.count(PlayerColor::NEUTRAL)) loadNeutralBattleAI(); } } void CClient::handlePack( CPack * pack ) { CBaseForCLApply *apply = applier->apps[typeList.getTypeID(pack)]; //find the applier if(apply) { boost::unique_lock guiLock(*LOCPLINT->pim); apply->applyOnClBefore(this,pack); logNetwork->traceStream() << "\tMade first apply on cl"; gs->apply(pack); logNetwork->traceStream() << "\tApplied on gs"; apply->applyOnClAfter(this,pack); logNetwork->traceStream() << "\tMade second apply on cl"; } else { logNetwork->errorStream() << "Message cannot be applied, cannot find applier! TypeID " << typeList.getTypeID(pack); } delete pack; } void CClient::finishCampaign( shared_ptr camp ) { } void CClient::proposeNextMission(shared_ptr camp) { GH.pushInt(new CBonusSelection(camp)); } void CClient::stopConnection() { terminate = true; if (serv) //request closing connection { logNetwork->infoStream() << "Connection has been requested to be closed."; boost::unique_lock(*serv->wmx); CloseServer close_server; sendRequest(&close_server, PlayerColor::NEUTRAL); logNetwork->infoStream() << "Sent closing signal to the server"; } if(connectionHandler)//end connection handler { if(connectionHandler->get_id() != boost::this_thread::get_id()) connectionHandler->join(); logNetwork->infoStream() << "Connection handler thread joined"; delete connectionHandler; connectionHandler = nullptr; } if (serv) //and delete connection { serv->close(); delete serv; serv = nullptr; logNetwork->warnStream() << "Our socket has been closed."; } } 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); 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(!gNoGUI && (!!att || !!def || gs->scenarioOps->mode == StartInfo::DUEL)) { boost::unique_lock un(*LOCPLINT->pim); auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, Rect((screen->w - 800)/2, (screen->h - 600)/2, 800, 600), att, def); GH.pushInt(bi); } 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(info->tacticDistance && vstd::contains(battleints,info->sides[info->tacticsSide].color)) { boost::thread(&CClient::commenceTacticPhaseForInt, this, battleints[info->sides[info->tacticsSide].color]); } } void CClient::battleFinished() { for(auto & side : gs->curB->sides) if(battleCallbacks.count(side.color)) battleCallbacks[side.color]->setBattle(nullptr); } void CClient::loadNeutralBattleAI() { installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); } void CClient::commitPackage( CPackForClient *pack ) { CommitPackage cp; cp.freePack = false; cp.packToCommit = pack; sendRequest(&cp, PlayerColor::NEUTRAL); } PlayerColor CClient::getLocalPlayer() const { if(LOCPLINT) return LOCPLINT->playerID; return getCurrentPlayer(); } void CClient::commenceTacticPhaseForInt(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))); sendRequest(&ma, battleInt->playerID); } } catch(...) { handleException(); } } void CClient::invalidatePaths() { // turn pathfinding info into invalid. It will be regenerated later boost::unique_lock pathLock(pathInfo->pathMx); pathInfo->hero = nullptr; } const CPathsInfo * CClient::getPathsInfo(const CGHeroInstance *h) { assert(h); boost::unique_lock pathLock(pathInfo->pathMx); if (pathInfo->hero != h) { gs->calculatePaths(h, *pathInfo.get()); } return pathInfo.get(); } int CClient::sendRequest(const CPack *request, PlayerColor player) { static ui32 requestCounter = 0; ui32 requestID = requestCounter++; logNetwork->traceStream() << boost::format("Sending a request \"%s\". It'll have an ID=%d.") % typeid(*request).name() % requestID; waitingRequest.pushBack(requestID); serv->sendPackToServer(*request, player, requestID); if(vstd::contains(playerint, player)) playerint[player]->requestSent(dynamic_cast(request), requestID); return requestID; } void CClient::campaignMapFinished( shared_ptr camp ) { endGame(false); GH.curInt = CGPreGame::create(); auto & epilogue = camp->camp->scenarios[camp->mapsConquered.back()].epilog; auto finisher = [=]() { if(camp->mapsRemaining.size()) proposeNextMission(camp); else finishCampaign(camp); }; if(epilogue.hasPrologEpilog) { GH.pushInt(new CPrologEpilogVideo(epilogue, finisher)); } else { finisher(); } } void CClient::installNewPlayerInterface(shared_ptr gameInterface, boost::optional color) { boost::unique_lock un(*LOCPLINT->pim); PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE); if(!color) privilagedGameEventReceivers.push_back(gameInterface); playerint[colorUsed] = gameInterface; logGlobal->traceStream() << boost::format("\tInitializing the interface for player %s") % colorUsed; auto cb = make_shared(gs, color, this); callbacks[colorUsed] = cb; battleCallbacks[colorUsed] = cb; gameInterface->init(cb); installNewBattleInterface(gameInterface, color, false); } void CClient::installNewBattleInterface(shared_ptr battleInterface, boost::optional color, bool needCallback /*= true*/) { boost::unique_lock un(*LOCPLINT->pim); PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE); if(!color) privilagedBattleEventReceivers.push_back(battleInterface); battleints[colorUsed] = battleInterface; if(needCallback) { logGlobal->traceStream() << boost::format("\tInitializing the battle interface for player %s") % *color; auto cbc = make_shared(gs, color, this); battleCallbacks[colorUsed] = cbc; battleInterface->init(cbc); } } std::string CClient::aiNameForPlayer(const PlayerSettings &ps, bool battleAI) { if(ps.name.size()) { const boost::filesystem::path aiPath = VCMIDirs::get().libraryPath() / "AI" / VCMIDirs::get().libraryName(ps.name); if (boost::filesystem::exists(aiPath)) return ps.name; } 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 CServerHandler::startServer() { th.update(); serverThread = new boost::thread(&CServerHandler::callServer, this); //runs server executable; if(verbose) logNetwork->infoStream() << "Setting up thread calling server: " << th.getDiff(); } void CServerHandler::waitForServer() { if(!serverThread) startServer(); th.update(); #ifndef VCMI_ANDROID intpr::scoped_lock slock(shared->sr->mutex); while(!shared->sr->ready) { shared->sr->cond.wait(slock); } #endif if(verbose) logNetwork->infoStream() << "Waiting for server: " << th.getDiff(); } CConnection * CServerHandler::connectToServer() { #ifndef VCMI_ANDROID if(!shared->sr->ready) waitForServer(); #else waitForServer(); #endif th.update(); //put breakpoint here to attach to server before it does something stupid CConnection *ret = justConnectToServer(settings["server"]["server"].String(), port); if(verbose) logNetwork->infoStream()<<"\tConnecting to the server: "<(settings["server"]["port"].Float()); verbose = true; #ifndef VCMI_ANDROID boost::interprocess::shared_memory_object::remove("vcmi_memory"); //if the application has previously crashed, the memory may not have been removed. to avoid problems - try to destroy it try { shared = new SharedMem(); } catch(...) { logNetwork->error("Cannot open interprocess memory."); handleException(); throw; } #endif } CServerHandler::~CServerHandler() { delete shared; delete serverThread; //detaches, not kills thread } void CServerHandler::callServer() { setThreadName("CServerHandler::callServer"); const std::string logName = (VCMIDirs::get().userCachePath() / "server_log.txt").string(); const std::string comm = VCMIDirs::get().serverPath().string() + " --port=" + port + " > \"" + logName + '\"'; int result = std::system(comm.c_str()); if (result == 0) logNetwork->infoStream() << "Server closed correctly"; else { logNetwork->errorStream() << "Error: server failed to close correctly or crashed!"; logNetwork->errorStream() << "Check " << logName << " for more info"; exit(1);// exit in case of error. Othervice without working server VCMI will hang } } CConnection * CServerHandler::justConnectToServer(const std::string &host, const std::string &port) { CConnection *ret = nullptr; while(!ret) { try { logNetwork->infoStream() << "Establishing connection..."; ret = new CConnection( host.size() ? host : settings["server"]["server"].String(), port.size() ? port : boost::lexical_cast(settings["server"]["port"].Float()), NAME); } catch(...) { logNetwork->errorStream() << "\nCannot establish connection! Retrying within 2 seconds"; SDL_Delay(2000); } } return ret; } vcmi-0.98/client/Client.h000066400000000000000000000257001250671757600153010ustar00rootroot00000000000000#pragma once #include "../lib/IGameCallback.h" #include "../lib/BattleAction.h" #include "../lib/CStopWatch.h" #include "../lib/int3.h" /* * 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 * */ class CPack; class CCampaignState; class CBattleCallback; class IGameEventsReceiver; class IBattleEventsReceiver; class CBattleGameInterface; struct StartInfo; class CGameState; class CGameInterface; class CConnection; class CCallback; struct BattleAction; struct SharedMem; class CClient; class CScriptingModule; struct CPathsInfo; class CISer; class COSer; namespace boost { class thread; } /// structure to handle running server and connecting to it class CServerHandler { private: void callServer(); //calls server via system(), should be called as thread public: CStopWatch th; boost::thread *serverThread; //thread that called system to run server SharedMem *shared; //interprocess memory (for waiting for server) bool verbose; //whether to print log msgs std::string port; //port number in text form //functions setting up local server void startServer(); //creates a thread with callServer void waitForServer(); //waits till server is ready CConnection * connectToServer(); //connects to server ////////////////////////////////////////////////////////////////////////// static CConnection * justConnectToServer(const std::string &host = "", const std::string &port = ""); //connects to given host without taking any other actions (like setting up server) CServerHandler(bool runServer = false); ~CServerHandler(); }; template class ThreadSafeVector { typedef std::vector TVector; typedef boost::unique_lock TLock; TVector items; boost::mutex mx; boost::condition_variable cond; public: 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 { unique_ptr pathInfo; public: std::map > callbacks; //callbacks given to player interfaces std::map > battleCallbacks; //callbacks given to player interfaces std::vector> privilagedGameEventReceivers; //scripting modules, spectator interfaces std::vector> privilagedBattleEventReceivers; //scripting modules, spectator interfaces std::map> playerint; std::map> battleints; std::map>> additionalPlayerInts; std::map>> additionalBattleInts; bool hotSeat; CConnection *serv; boost::optional curbaction; CScriptingModule *erm; ThreadSafeVector waitingRequest; void waitForMoveAndSend(PlayerColor color); //void sendRequest(const CPackForServer *request, bool waitForRealization); CClient(void); CClient(CConnection *con, StartInfo *si); ~CClient(void); void init(); void newGame(CConnection *con, StartInfo *si); //con - connection to server void loadNeutralBattleAI(); void installNewPlayerInterface(shared_ptr gameInterface, boost::optional color); void installNewBattleInterface(shared_ptr battleInterface, boost::optional color, bool needCallback = true); std::string aiNameForPlayer(const PlayerSettings &ps, bool battleAI); //empty means no AI -> human void endGame(bool closeConnection = true); void stopConnection(); void save(const std::string & fname); void loadGame(const std::string & fname, const bool server = true, const std::vector& humanplayerindices = std::vector(), const int loadnumplayers = 1, int player_ = -1, const std::string & ipaddr = "", const std::string & port = ""); void run(); void campaignMapFinished( shared_ptr camp ); void finishCampaign( shared_ptr camp ); void proposeNextMission(shared_ptr camp); void invalidatePaths(); const CPathsInfo * getPathsInfo(const CGHeroInstance *h); bool terminate; // tell to terminate boost::thread *connectionHandler; //thread running run() method ////////////////////////////////////////////////////////////////////////// virtual PlayerColor getLocalPlayer() const override; ////////////////////////////////////////////////////////////////////////// //not working yet, will be implement somewhen later with support for local-sim-based gameplay 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, 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){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 giveHeroArtifact(int artid, int hid, int position){}; //void giveNewArtifact(int hid, int position){}; 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 * info) 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 {} ////////////////////////////////////////////////////////////////////////// friend class CCallback; //handling players actions friend class CBattleCallback; //handling players actions int sendRequest(const CPack *request, PlayerColor player); //returns ID given to that request void handlePack( CPack * pack ); //applies the given pack and deletes it void battleStarted(const BattleInfo * info); void commenceTacticPhaseForInt(shared_ptr battleInt); //will be called as separate thread void commitPackage(CPackForClient *pack) override; ////////////////////////////////////////////////////////////////////////// void serialize(COSer &h, const int version); void serialize(CISer &h, const int version); void serialize(COSer &h, const int version, const std::set& playerIDs); void serialize(CISer &h, const int version, const std::set& playerIDs); void battleFinished(); }; vcmi-0.98/client/DPIaware.manifest000066400000000000000000000006241250671757600170740ustar00rootroot00000000000000 true vcmi-0.98/client/Graphics.cpp000066400000000000000000000317771250671757600161710ustar00rootroot00000000000000#include "StdInc.h" #include "Graphics.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/CBinaryReader.h" #include "CDefHandler.h" #include "gui/SDL_Extensions.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/spells/CSpellHandler.h" #include "../lib/CGameState.h" #include "../lib/JsonNode.h" #include "../lib/vcmi_endian.h" #include "../lib/GameConstants.h" #include "../lib/CStopWatch.h" #include "../lib/mapObjects/CObjectClassesHandler.h" using namespace CSDL_Ext; #ifdef min #undef min #endif #ifdef max #undef max #endif /* * 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 * */ 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++]; CSDL_Ext::colorSetAlpha(col,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 CSDL_Ext::colorSetAlpha(neutralColorPalette[i], 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]; } neutralColor->r = 0x84; neutralColor->g = 0x84; neutralColor->b = 0x84; //gray CSDL_Ext::colorSetAlpha(*neutralColor,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::loadHeroFlags,this); tasks += std::bind(&Graphics::initializeBattleGraphics,this); tasks += std::bind(&Graphics::loadErmuToPicture,this); tasks += std::bind(&Graphics::initializeImageLists,this); tasks += GET_DEF_ESS(resources32,"RESOURCE.DEF"); tasks += GET_DEF_ESS(heroMoveArrows,"ADAG.DEF"); CThreadHelper th(&tasks,std::max((ui32)1,boost::thread::hardware_concurrency())); th.run(); #else loadFonts(); loadPaletteAndColors(); loadHeroFlags(); initializeBattleGraphics(); loadErmuToPicture(); initializeImageLists(); resources32 = CDefHandler::giveDefEss("RESOURCE.DEF"); heroMoveArrows = CDefHandler::giveDefEss("ADAG.DEF"); #endif for(auto & elem : heroMoveArrows->ourImages) { CSDL_Ext::alphaTransform(elem.bitmap); } } void Graphics::loadHeroAnims() { //first - group number to be rotated1, second - group number after rotation1 std::vector > rotations = { {6,10}, {7,11}, {8,12}, {1,13}, {2,14}, {3,15} }; for(auto & elem : CGI->heroh->classes.heroClasses) { for (auto & templ : VLC->objtypeh->getHandlerFor(Obj::HERO, elem->id)->getTemplates()) { if (!heroAnims.count(templ.animationFile)) heroAnims[templ.animationFile] = loadHeroAnim(templ.animationFile, rotations); } } boatAnims.push_back(loadHeroAnim("AB01_.DEF", rotations)); boatAnims.push_back(loadHeroAnim("AB02_.DEF", rotations)); boatAnims.push_back(loadHeroAnim("AB03_.DEF", rotations)); } CDefEssential * Graphics::loadHeroAnim( const std::string &name, const std::vector > &rotations) { CDefEssential *anim = CDefHandler::giveDefEss(name); int pom = 0; //how many groups has been rotated for(int o=7; pom<6; ++o) { for(int p=0;p<6;p++) { if(anim->ourImages[o].groupNumber == rotations[p].first) { for(int e=0; e<8; ++e) { Cimage nci; nci.bitmap = CSDL_Ext::verticalFlip(anim->ourImages[o+e].bitmap); nci.groupNumber = rotations[p].second; nci.imName = std::string(); anim->ourImages.push_back(nci); if(pom>2) //we need only one frame for groups 13/14/15 break; } if(pom<3) //there are eight frames of animtion of groups 6/7/8 so for speed we'll skip them o+=8; else //there is only one frame of 1/2/3 o+=1; ++pom; if(p==2 && pom<4) //group1 starts at index 1 o = 1; } } } for(auto & elem : anim->ourImages) { CSDL_Ext::alphaTransform(elem.bitmap); } return anim; } void Graphics::loadHeroFlagsDetail(std::pair Graphics::*, std::vector > &pr, bool mode) { for(int i=0;i<8;i++) (this->*pr.first).push_back(CDefHandler::giveDefEss(pr.second[i])); //first - group number to be rotated1, second - group number after rotation1 std::vector > rotations = { {6,10}, {7,11}, {8,12} }; for(int q=0; q<8; ++q) { std::vector &curImgs = (this->*pr.first)[q]->ourImages; for(size_t o=0; o Graphics::*, std::vector > pr[4] = { { &Graphics::flags1, {"ABF01L.DEF","ABF01G.DEF","ABF01R.DEF","ABF01D.DEF","ABF01B.DEF", "ABF01P.DEF","ABF01W.DEF","ABF01K.DEF"} }, { &Graphics::flags2, {"ABF02L.DEF","ABF02G.DEF","ABF02R.DEF","ABF02D.DEF","ABF02B.DEF", "ABF02P.DEF","ABF02W.DEF","ABF02K.DEF"} }, { &Graphics::flags3, {"ABF03L.DEF","ABF03G.DEF","ABF03R.DEF","ABF03D.DEF","ABF03B.DEF", "ABF03P.DEF","ABF03W.DEF","ABF03K.DEF"} }, { &Graphics::flags4, {"AF00.DEF","AF01.DEF","AF02.DEF","AF03.DEF","AF04.DEF", "AF05.DEF","AF06.DEF","AF07.DEF"} } }; #if 0 boost::thread_group grupa; for(int g=3; g>=0; --g) { grupa.create_thread(std::bind(&Graphics::loadHeroFlagsDetail, this, std::ref(pr[g]), true)); } grupa.join_all(); #else for(auto p: pr) { loadHeroFlagsDetail(p,true); } #endif logGlobal->infoStream() << "Loading and transforming heroes' flags: "<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->errorStream() << "Wrong player id in blueToPlayersAdv (" << player << ")!"; return; } SDL_SetColors(sur, palette, 224, 32); } 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->warnStream() << "Image must have palette to be player-colored!"; } } 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; iappearance.animationFile]; } CDefEssential * Graphics::getDef( const ObjectTemplate & info ) { return advmapobjGraphics[info.animationFile]; } 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); } } vcmi-0.98/client/Graphics.h000066400000000000000000000060721250671757600156240ustar00rootroot00000000000000#pragma once #include "gui/Fonts.h" #include "../lib/GameConstants.h" #include "gui/Geometries.h" /* * 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 * */ class CDefEssential; struct SDL_Surface; class CGHeroInstance; class CGTownInstance; class CDefHandler; 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); public: //Fonts static const int FONTS_NUMBER = 9; IFont * fonts[FONTS_NUMBER]; //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::vector flags1, flags2, flags3, flags4; //flags blitted on heroes when , CDefEssential * resources32; //resources 32x32 CDefEssential * heroMoveArrows; std::map heroAnims; // [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::vector boatAnims; // [boat type: 0 - 3] //added group 10: up - left, 11 - left and 12 - left down // 13 - up-left standing; 14 - left standing; 15 - left down standing CDefHandler * FoWfullHide; //for Fog of War CDefHandler * FoWpartialHide; //for For of War std::map imageLists; std::map advmapobjGraphics; CDefEssential * getDef(const CGObjectInstance * obj); CDefEssential * getDef(const ObjectTemplate & info); //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(); void initializeBattleGraphics(); void loadPaletteAndColors(); void loadHeroFlags(); void loadHeroFlagsDetail(std::pair Graphics::*, std::vector > &pr, bool mode); void loadHeroAnims(); CDefEssential * loadHeroAnim(const std::string &name, const std::vector > &rotations); void loadErmuToPicture(); void blueToPlayersAdv(SDL_Surface * sur, PlayerColor player); //replaces blue interface colour with a color of player void loadFonts(); void initializeImageLists(); }; extern Graphics * graphics; vcmi-0.98/client/Info.plist000066400000000000000000000010471250671757600156600ustar00rootroot00000000000000 CFBundleIconFile vcmi.icns CFBundleVersion ${VCMI_VERSION_MAJOR}.${VCMI_VERSION_MINOR} CFBundleIdentifier com.vcmi.vcmiclient SUFeedURL http://vcmi.eu/sparkle_appcast.xml SUPublicDSAKeyFile vcmi_dsa_public.pem vcmi-0.98/client/NetPacksClient.cpp000066400000000000000000000655401250671757600172730ustar00rootroot00000000000000#include "StdInc.h" #include "../lib/NetPacks.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/CFileInfo.h" #include "../CCallback.h" #include "Client.h" #include "CPlayerInterface.h" #include "CGameInfo.h" #include "../lib/Connection.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/BattleState.h" #include "../lib/GameConstants.h" #include "gui/CGuiHandler.h" #include "widgets/MiscWidgets.h" #include "widgets/AdventureMapClasses.h" #include "CMT.h" //macros to avoid code duplication - calls given method with given arguments if interface for specific player is present //awaiting variadic templates... #define CALL_IN_PRIVILAGED_INTS(function, ...) \ do \ { \ for(auto &ger : cl->privilagedGameEventReceivers) \ ger->function(__VA_ARGS__); \ } while(0) #define CALL_ONLY_THAT_INTERFACE(player, function, ...) \ do \ { \ if(vstd::contains(cl->playerint,player)) \ cl->playerint[player]->function(__VA_ARGS__); \ }while(0) #define INTERFACE_CALL_IF_PRESENT(player,function,...) \ do \ { \ CALL_ONLY_THAT_INTERFACE(player, function, __VA_ARGS__);\ CALL_IN_PRIVILAGED_INTS(function, __VA_ARGS__); \ } while(0) #define CALL_ONLY_THAT_BATTLE_INTERFACE(player,function, ...) \ do \ { \ if(vstd::contains(cl->battleints,player)) \ cl->battleints[player]->function(__VA_ARGS__); \ \ if(cl->additionalBattleInts.count(player)) \ { \ for(auto bInt : cl->additionalBattleInts[player])\ bInt->function(__VA_ARGS__); \ } \ } while (0); #define BATTLE_INTERFACE_CALL_RECEIVERS(function,...) \ do \ { \ for(auto & ber : cl->privilagedBattleEventReceivers)\ ber->function(__VA_ARGS__); \ } while(0) #define BATTLE_INTERFACE_CALL_IF_PRESENT(player,function,...) \ do \ { \ CALL_ONLY_THAT_INTERFACE(player, function, __VA_ARGS__);\ BATTLE_INTERFACE_CALL_RECEIVERS(function, __VA_ARGS__); \ } while(0) //calls all normal interfaces and privilaged ones, playerints may be updated when iterating over it, so we need a copy #define CALL_IN_ALL_INTERFACES(function, ...) \ do \ { \ auto ints = cl->playerint; \ for(auto i = ints.begin(); i != ints.end(); i++)\ CALL_ONLY_THAT_INTERFACE(i->first, function, __VA_ARGS__); \ } while(0) #define BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(function,...) \ CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[0].color, function, __VA_ARGS__) \ CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[1].color, function, __VA_ARGS__) \ BATTLE_INTERFACE_CALL_RECEIVERS(function, __VA_ARGS__) /* * 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 * */ void SetResources::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(player,receivedResource,-1,-1); } void SetResource::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(player,receivedResource,resid,val); } void SetPrimSkill::applyCl( CClient *cl ) { const CGHeroInstance *h = cl->getHero(id); if(!h) { logNetwork->errorStream() << "Cannot find hero with ID " << id.getNum(); return; } INTERFACE_CALL_IF_PRESENT(h->tempOwner,heroPrimarySkillChanged,h,which,val); } void SetSecSkill::applyCl( CClient *cl ) { const CGHeroInstance *h = cl->getHero(id); if(!h) { logNetwork->errorStream() << "Cannot find hero with ID " << id; return; } INTERFACE_CALL_IF_PRESENT(h->tempOwner,heroSecondarySkillChanged,h,which,val); } void HeroVisitCastle::applyCl( CClient *cl ) { const CGHeroInstance *h = cl->getHero(hid); if(start()) { INTERFACE_CALL_IF_PRESENT(h->tempOwner, 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); INTERFACE_CALL_IF_PRESENT(h->tempOwner, heroManaPointsChanged, h); } void SetMovePoints::applyCl( CClient *cl ) { const CGHeroInstance *h = cl->getHero(hid); cl->invalidatePaths(); INTERFACE_CALL_IF_PRESENT(h->tempOwner, 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? } void ChangeStackCount::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(sl.army->tempOwner, stackChagedCount, sl, count, absoluteValue); } void SetStackType::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(sl.army->tempOwner, stackChangedType, sl, *type); } void EraseStack::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(sl.army->tempOwner, stacksErased, sl); } void SwapStacks::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(sl1.army->tempOwner, stacksSwapped, sl1, sl2); if(sl1.army->tempOwner != sl2.army->tempOwner) INTERFACE_CALL_IF_PRESENT(sl2.army->tempOwner, stacksSwapped, sl1, sl2); } void InsertNewStack::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(sl.army->tempOwner,newStackInserted,sl, *sl.getStack()); } void RebalanceStacks::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(src.army->tempOwner, stacksRebalanced, src, dst, count); if(src.army->tempOwner != dst.army->tempOwner) INTERFACE_CALL_IF_PRESENT(dst.army->tempOwner,stacksRebalanced, src, dst, count); } void PutArtifact::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(al.owningPlayer(), artifactPut, al); } void EraseArtifact::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(al.owningPlayer(), artifactRemoved, al); } void MoveArtifact::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(src.owningPlayer(), artifactMoved, src, dst); if(src.owningPlayer() != dst.owningPlayer()) INTERFACE_CALL_IF_PRESENT(dst.owningPlayer(), artifactMoved, src, dst); } void AssembledArtifact::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(al.owningPlayer(), artifactAssembled, al); } void DisassembledArtifact::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(al.owningPlayer(), artifactDisassembled, al); } void HeroVisit::applyCl( CClient *cl ) { assert(hero); INTERFACE_CALL_IF_PRESENT(player, 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)); INTERFACE_CALL_IF_PRESENT(h->tempOwner, heroBonusChanged, h, *h->getBonusList().back(),true); } break; case PLAYER: { const PlayerState *p = GS(cl)->getPlayer(PlayerColor(id)); INTERFACE_CALL_IF_PRESENT(PlayerColor(id), playerBonusChanged, *p->getBonusList().back(), true); } break; } } void ChangeObjPos::applyFirstCl( CClient *cl ) { CGObjectInstance *obj = GS(cl)->getObjInstance(objid); if(flags & 1) CGI->mh->hideObject(obj); } void ChangeObjPos::applyCl( CClient *cl ) { CGObjectInstance *obj = GS(cl)->getObjInstance(objid); if(flags & 1) CGI->mh->printObject(obj); cl->invalidatePaths(); } void PlayerEndsGame::applyCl( CClient *cl ) { CALL_IN_ALL_INTERFACES(gameOver, player, victoryLossCheckResult); } void RemoveBonus::applyCl( CClient *cl ) { cl->invalidatePaths(); switch(who) { case HERO: { const CGHeroInstance *h = GS(cl)->getHero(ObjectInstanceID(id)); INTERFACE_CALL_IF_PRESENT(h->tempOwner, heroBonusChanged, h, bonus,false); } break; case PLAYER: { //const PlayerState *p = GS(cl)->getPlayer(id); INTERFACE_CALL_IF_PRESENT(PlayerColor(id), playerBonusChanged, bonus, false); } break; } } void UpdateCampaignState::applyCl( CClient *cl ) { cl->stopConnection(); cl->campaignMapFinished(camp); } void PrepareForAdvancingCampaign::applyCl(CClient *cl) { cl->serv->prepareForSendingHeroes(); } void RemoveObject::applyFirstCl( CClient *cl ) { const CGObjectInstance *o = cl->getObj(id); CGI->mh->hideObject(o, true); //notify interfaces about removal for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++) { if(GS(cl)->isVisible(o, 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++) { if(i->first >= PlayerColor::PLAYER_LIMIT) continue; TeamState *t = GS(cl)->getPlayerTeam(i->first); if((t->fogOfWarMap[start.x-1][start.y][start.z] || t->fogOfWarMap[end.x-1][end.y][end.z]) && GS(cl)->getPlayer(i->first)->human) humanKnows = true; } if(result == TELEPORTATION || result == EMBARK || result == DISEMBARK || !humanKnows) CGI->mh->removeObject(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(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(i->first >= PlayerColor::PLAYER_LIMIT) continue; TeamState *t = GS(cl)->getPlayerTeam(i->first); if(t->fogOfWarMap[start.x-1][start.y][start.z] || t->fogOfWarMap[end.x-1][end.y][end.z]) { i->second->heroMoved(*this); } } if(!humanKnows) //maphandler didn't get update from playerint, do it now { //TODO: restructure nicely CGI->mh->printObject(h); } } void NewStructures::applyCl( CClient *cl ) { CGTownInstance *town = GS(cl)->getTown(tid); for(const auto & id : bid) { town->updateAppearance(); if(vstd::contains(cl->playerint,town->tempOwner)) cl->playerint[town->tempOwner]->buildChanged(town,id,1); } } void RazeStructures::applyCl (CClient *cl) { CGTownInstance *town = GS(cl)->getTown(tid); for(const auto & id : bid) { town->updateAppearance(); if(vstd::contains (cl->playerint,town->tempOwner)) cl->playerint[town->tempOwner]->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; INTERFACE_CALL_IF_PRESENT(p, 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); std::set playersToNotify; if(vstd::contains(cl->playerint,t->tempOwner)) // our town playersToNotify.insert(t->tempOwner); if (hGarr && vstd::contains(cl->playerint, hGarr->tempOwner)) playersToNotify.insert(hGarr->tempOwner); if (hVisit && vstd::contains(cl->playerint, hVisit->tempOwner)) playersToNotify.insert(hVisit->tempOwner); for(auto playerID : playersToNotify) cl->playerint[playerID]->heroInGarrisonChange(t); } // void SetHeroArtifacts::applyCl( CClient *cl ) // { // // CGHeroInstance *h = GS(cl)->getHero(hid); // // CGameInterface *player = (vstd::contains(cl->playerint,h->tempOwner) ? cl->playerint[h->tempOwner] : nullptr); // // if(!player) // // return; // // //h->recreateArtBonuses(); // //player->heroArtifactSetChanged(h); // // // for(Bonus bonus : gained) // // { // // player->heroBonusChanged(h,bonus,true); // // } // // for(Bonus bonus : lost) // // { // // player->heroBonusChanged(h,bonus,false); // // } // } void HeroRecruited::applyCl( CClient *cl ) { CGHeroInstance *h = GS(cl)->map->heroesOnMap.back(); if(h->subID != hid) { logNetwork->errorStream() << "Something wrong with hero recruited!"; } CGI->mh->printObject(h); if(vstd::contains(cl->playerint,h->tempOwner)) { cl->playerint[h->tempOwner]->heroCreated(h); if(const CGTownInstance *t = GS(cl)->getTown(tid)) cl->playerint[h->tempOwner]->heroInGarrisonChange(t); } } void GiveHero::applyCl( CClient *cl ) { CGHeroInstance *h = GS(cl)->getHero(id); CGI->mh->printObject(h); cl->playerint[h->tempOwner]->heroCreated(h); } void GiveHero::applyFirstCl( CClient *cl ) { CGI->mh->hideObject(GS(cl)->getHero(id)); } void InfoWindow::applyCl( CClient *cl ) { std::vector comps; for(auto & elem : components) { comps.push_back(&elem); } std::string str; text.toString(str); if(vstd::contains(cl->playerint,player)) cl->playerint.at(player)->showInfoDialog(str,comps,(soundBase::soundID)soundID); else logNetwork->warnStream() << "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)) INTERFACE_CALL_IF_PRESENT(it->first, objectPropertyChanged, this); } } void HeroLevelUp::applyCl( CClient *cl ) { //INTERFACE_CALL_IF_PRESENT(h->tempOwner, heroGotLevel, h, primskill, skills, id); if(vstd::contains(cl->playerint,hero->tempOwner)) { cl->playerint[hero->tempOwner]->heroGotLevel(hero, primskill, skills, queryID); } //else // cb->selectionMade(0, queryID); } void CommanderLevelUp::applyCl( CClient *cl ) { const CCommanderInstance * commander = hero->commander; assert (commander); PlayerColor player = hero->tempOwner; if (commander->armyObj && vstd::contains(cl->playerint, player)) //is it possible for Commander to exist beyond armed instance? { cl->playerint[player]->commanderGotLevel(commander, skills, queryID); } } void BlockingDialog::applyCl( CClient *cl ) { std::string str; text.toString(str); if(vstd::contains(cl->playerint,player)) cl->playerint.at(player)->showBlockingDialog(str,components,queryID,(soundBase::soundID)soundID,selection(),cancel()); else logNetwork->warnStream() << "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)); if(!vstd::contains(cl->playerint,h->getOwner())) return; cl->playerint.at(h->getOwner())->showGarrisonDialog(obj,h,removableUnits,queryID); } void ExchangeDialog::applyCl(CClient *cl) { assert(heroes[0] && heroes[1]); INTERFACE_CALL_IF_PRESENT(heroes[0]->tempOwner, heroExchangeStarted, heroes[0]->id, heroes[1]->id, queryID); } void TeleportDialog::applyCl( CClient *cl ) { CALL_ONLY_THAT_INTERFACE(hero->tempOwner,showTeleportDialog,channel,exits,impassable,queryID); } void BattleStart::applyFirstCl( CClient *cl ) { //Cannot use the usual macro because curB is not set yet CALL_ONLY_THAT_BATTLE_INTERFACE(info->sides[0].color, battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject, info->tile, info->sides[0].hero, info->sides[1].hero); CALL_ONLY_THAT_BATTLE_INTERFACE(info->sides[1].color, battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject, info->tile, info->sides[0].hero, info->sides[1].hero); BATTLE_INTERFACE_CALL_RECEIVERS(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) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleNewRoundFirst,round); } void BattleNextRound::applyCl( CClient *cl ) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(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; } if( vstd::contains(cl->battleints, playerToCall) ) boost::thread( std::bind(&CClient::waitForMoveAndSend, cl, playerToCall) ); } void BattleTriggerEffect::applyCl(CClient * cl) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleTriggerEffect, *this); } void BattleObstaclePlaced::applyCl(CClient * cl) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleObstaclePlaced, *obstacle); } void BattleResult::applyFirstCl( CClient *cl ) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleEnd,this); cl->battleFinished(); } void BattleStackMoved::applyFirstCl( CClient *cl ) { const CStack * movedStack = GS(cl)->curB->battleGetStackByID(stack); BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleStackMoved,movedStack,tilesToMove,distance); } //void BattleStackAttacked::( CClient *cl ) void BattleStackAttacked::applyFirstCl( CClient *cl ) { std::vector bsa; bsa.push_back(*this); BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleStacksAttacked,bsa); } void BattleAttack::applyFirstCl( CClient *cl ) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleAttack,this); for (auto & elem : bsa) { for (int z=0; zcurbaction = ba; BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(actionStarted, ba); } void BattleSpellCast::applyCl( CClient *cl ) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleSpellCast,this); } void SetStackEffect::applyCl( CClient *cl ) { //informing about effects BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleStacksEffectsSet,*this); } void StacksInjured::applyCl( CClient *cl ) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleStacksAttacked,stacks); } void BattleResultsApplied::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(player1, battleResultsApplied); INTERFACE_CALL_IF_PRESENT(player2, battleResultsApplied); INTERFACE_CALL_IF_PRESENT(PlayerColor::UNFLAGGABLE, battleResultsApplied); if(GS(cl)->initialOpts->mode == StartInfo::DUEL) { handleQuit(); } } void StacksHealedOrResurrected::applyCl( CClient *cl ) { std::vector > shiftedHealed; for(auto & elem : healedStacks) { shiftedHealed.push_back(std::make_pair(elem.stackID, elem.healedHP)); } BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleStacksHealedRes, shiftedHealed, lifeDrain, tentHealing, drainedFrom); } void ObstaclesRemoved::applyCl( CClient *cl ) { //inform interfaces about removed obstacles BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleObstaclesRemoved, obstacles); } void CatapultAttack::applyCl( CClient *cl ) { //inform interfaces about catapult attack BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleCatapultAttacked, *this); } void BattleStacksRemoved::applyCl( CClient *cl ) { //inform interfaces about removed stacks BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleStacksRemoved, *this); } void BattleStackAdded::applyCl( CClient *cl ) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleNewStackAppeared, GS(cl)->curB->stacks.back()); } CGameState* CPackForClient::GS( CClient *cl ) { return cl->gs; } void EndAction::applyCl( CClient *cl ) { BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(actionFinished, *cl->curbaction); cl->curbaction.reset(); } void PackageApplied::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(player, requestRealized, this); if(!cl->waitingRequest.tryRemovingElement(requestID)) logNetwork->warnStream() << "Surprising server message!"; } void SystemMessage::applyCl( CClient *cl ) { std::ostringstream str; str << "System message: " << text; logNetwork->errorStream() << str.str(); // usually used to receive error messages from server if(LOCPLINT) LOCPLINT->cingconsole->print(str.str()); } void PlayerBlocked::applyCl( CClient *cl ) { INTERFACE_CALL_IF_PRESENT(player,playerBlocked,reason, startOrEnd==BLOCKADE_STARTED); } void YourTurn::applyCl( CClient *cl ) { CALL_IN_ALL_INTERFACES(playerStartsTurn, player); CALL_ONLY_THAT_INTERFACE(player,yourTurn); } void SaveGame::applyCl(CClient *cl) { CFileInfo info(fname); CResourceHandler::get("local")->createResource(info.getStem() + ".vcgm1"); try { CSaveFile save(*CResourceHandler::get()->getResourceName(ResourceID(info.getStem(), EResType::CLIENT_SAVEGAME))); cl->saveCommonState(save); save << *cl; } catch(std::exception &e) { logNetwork->errorStream() << "Failed to save game:" << e.what(); } } void PlayerMessage::applyCl(CClient *cl) { std::ostringstream str; str << "Player "<< player <<" sends a message: " << text; logNetwork->debugStream() << str.str(); if(LOCPLINT) LOCPLINT->cingconsole->print(str.str()); } void ShowInInfobox::applyCl(CClient *cl) { INTERFACE_CALL_IF_PRESENT(player,showComp, c, text.toString()); } void AdvmapSpellCast::applyCl(CClient *cl) { cl->invalidatePaths(); //consider notifying other interfaces that see hero? INTERFACE_CALL_IF_PRESENT(caster->getOwner(),advmapSpellCast, caster, spellID); } void ShowWorldViewEx::applyCl(CClient * cl) { CALL_ONLY_THAT_INTERFACE(player, 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))); INTERFACE_CALL_IF_PRESENT(dst->tempOwner,showRecruitmentDialog, dw, dst, window == RECRUITMENT_FIRST ? 0 : -1); } break; case SHIPYARD_WINDOW: { const IShipyard *sy = IShipyard::castFrom(cl->getObj(ObjectInstanceID(id1))); INTERFACE_CALL_IF_PRESENT(sy->o->tempOwner, showShipyardDialog, sy); } break; case THIEVES_GUILD: { //displays Thieves' Guild window (when hero enters Den of Thieves) const CGObjectInstance *obj = cl->getObj(ObjectInstanceID(id2)); INTERFACE_CALL_IF_PRESENT(PlayerColor(id1), 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)); INTERFACE_CALL_IF_PRESENT(hero->tempOwner,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); INTERFACE_CALL_IF_PRESENT(cl->getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, 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)); INTERFACE_CALL_IF_PRESENT(cl->getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, showHillFortWindow, obj, hero); } break; case PUZZLE_MAP: { INTERFACE_CALL_IF_PRESENT(PlayerColor(id1), showPuzzleMap); } break; case TAVERN_WINDOW: const CGObjectInstance *obj1 = cl->getObj(ObjectInstanceID(id1)), *obj2 = cl->getObj(ObjectInstanceID(id2)); INTERFACE_CALL_IF_PRESENT(obj1->tempOwner, showTavernWindow, obj2); break; } } void CenterView::applyCl(CClient *cl) { INTERFACE_CALL_IF_PRESENT (player, centerView, pos, focusTime); } void NewObject::applyCl(CClient *cl) { cl->invalidatePaths(); const CGObjectInstance *obj = cl->getObj(id); 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 { for(auto & elem : cl->playerint) elem.second->availableArtifactsChanged(nullptr); } else { const CGBlackMarket *bm = dynamic_cast(cl->getObj(ObjectInstanceID(id))); assert(bm); INTERFACE_CALL_IF_PRESENT(cl->getTile(bm->visitablePos())->visitableObjects.back()->tempOwner, availableArtifactsChanged, bm); } } void TradeComponents::applyCl(CClient *cl) {///Shop handler switch (CGI->mh->map->objects.at(objectid)->ID) { case Obj::BLACK_MARKET: break; case Obj::TAVERN: break; case Obj::DEN_OF_THIEVES: break; case Obj::TRADING_POST_SNOW: break; default: logNetwork->warnStream() << "Shop type not supported!"; } } vcmi-0.98/client/OSX.mm000066400000000000000000000002741250671757600147150ustar00rootroot00000000000000#import #import "Sparkle.h" void OSX_checkForUpdates() { SUUpdater* updater = [[SUUpdater alloc] init]; [[SUUpdater sharedUpdater] checkForUpdatesInBackground]; } vcmi-0.98/client/SDLMain.h000066400000000000000000000005661250671757600153150ustar00rootroot00000000000000/* 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.98/client/SDLMain.m000066400000000000000000000260001250671757600153110ustar00rootroot00000000000000/* 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.98/client/StdInc.cpp000066400000000000000000000000661250671757600156000ustar00rootroot00000000000000// Creates the precompiled header #include "StdInc.h"vcmi-0.98/client/StdInc.h000066400000000000000000000004121250671757600152400ustar00rootroot00000000000000#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.98/client/VCMI_client.cbp000066400000000000000000000204751250671757600165000ustar00rootroot00000000000000 vcmi-0.98/client/VCMI_client.rc000066400000000000000000000000341250671757600163250ustar00rootroot00000000000000IDI_ICON1 ICON "vcmi.ico"vcmi-0.98/client/VCMI_client.vcxproj000066400000000000000000000361241250671757600174250ustar00rootroot00000000000000 Debug Win32 Debug x64 RD Win32 RD x64 {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6} VCMI_client Application Unicode true v120_xp Application Unicode true v120_xp Application Unicode v120_xp Application Unicode v120_xp <_ProjectFileVersion>10.0.30128.1 .. $(VCMI_Out) $(Configuration)\ $(Configuration)\ .. $(VCMI_Out) $(Configuration)\ $(Configuration)\ AllRules.ruleset AllRules.ruleset AllRules.ruleset AllRules.ruleset $(SolutionDir)\..\include;$(IncludePath) 4251;%(DisableSpecificWarnings) NoListing Use StdInc.h /MP4 /Zm150 $(FFMPEGDIR)\include;. 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 /MP4 /Zm150 $(FFMPEGDIR)\include;.; 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 $(FFMPEGDIR)\lib;.;..\..\libs;.. false true /LTCG %(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.98/client/VCMI_client.vcxproj.filters000066400000000000000000000211361250671757600210710ustar00rootroot00000000000000 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 Source Files Header Files {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 vcmi-0.98/client/battle/000077500000000000000000000000001250671757600151615ustar00rootroot00000000000000vcmi-0.98/client/battle/CBattleAnimations.cpp000066400000000000000000000721251250671757600212350ustar00rootroot00000000000000#include "StdInc.h" #include "CBattleAnimations.h" #include #include "CBattleInterfaceClasses.h" #include "CBattleInterface.h" #include "CCreatureAnimation.h" #include "../CDefHandler.h" #include "../CGameInfo.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../Graphics.h" #include "../gui/CCursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/SDL_Extensions.h" #include "../../CCallback.h" #include "../../lib/BattleState.h" #include "../../lib/CTownHandler.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/spells/CSpellHandler.h" /* * 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 * */ CBattleAnimation::CBattleAnimation(CBattleInterface * _owner) : owner(_owner), ID(_owner->animIDhelper++) { logAnim->traceStream() << "Animation #" << ID << " created"; } CBattleAnimation::~CBattleAnimation() { logAnim->traceStream() << "Animation #" << ID << " deleted"; } void CBattleAnimation::endAnim() { logAnim->traceStream() << "Animation #" << ID << " ended, type is " << 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); CSpellEffectAnimation * thSen = dynamic_cast(this); for(auto & elem : owner->pendingAnims) { CBattleStackAnimation * stAnim = dynamic_cast(elem.first); CSpellEffectAnimation * 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) // 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), soundPlayed(false), dest(_dest), attackedStack(defender), attackingStack(attacker) { assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n"); bool isCatapultAttack = attackingStack->hasBonusOfType(Bonus::CATAPULT) && owner->getCurrentPlayerInterface()->cb->battleHexToWallPart(_dest) >= 0; assert(attackedStack || isCatapultAttack); UNUSED(isCatapultAttack); attackingStackPosBeforeReturn = attackingStack->position; } CDefenceAnimation::CDefenceAnimation(StackAttackedInfo _attackedInfo, CBattleInterface * _owner) : CBattleStackAnimation(_owner, _attackedInfo.defender), attacker(_attackedInfo.attacker), rangedAttack(_attackedInfo.indirectAttack), killed(_attackedInfo.killed) { logAnim->debugStream() << "Created defence anim for " << _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; CSpellEffectAnimation * 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->position, attacker->position, owner->creDir[stack->ID], attacker->doubleWide(), owner->creDir[attacker->ID])) { owner->addNewAnim(new CReverseAnimation(owner, stack, stack->position, 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], 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); if (stack->valOfBonuses(Selector::durationType(Bonus::STACK_GETS_TURN))) return battle_sound(stack->getCreature(), defend); return battle_sound(stack->getCreature(), wince); } CCreatureAnim::EAnimType CDefenceAnimation::getMyAnimType() { if(killed) return CCreatureAnim::DEATH; if (stack->valOfBonuses(Selector::durationType(Bonus::STACK_GETS_TURN).And(Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE)))) return CCreatureAnim::DEFENCE; 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) 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) {} 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->position, 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}; int revShiftattacker = (attackingStack->attackerOwned ? -1 : 1); int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest); if(mutPos == -1 && attackingStack->doubleWide()) { mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, attackedStack->position); } 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]; break; default: logGlobal->errorStream()<<"Critical Error! Wrong dest in stackAttacking! dest: "<debugStream() << "Created melee attack anim for " << 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->position), begX(0), begY(0), distanceX(0), distanceY(0), timeToMove(0.0), progress(0.0), nextHex(destTiles.front()) { logAnim->debugStream() << "Created movement anim for " << stack->getName(); } CMovementEndAnimation::CMovementEndAnimation(CBattleInterface * _owner, const CStack * _stack, BattleHex destTile) : CBattleStackAnimation(_owner, _stack), destinationTile(destTile) { logAnim->debugStream() << "Created movement end anim for " << 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->debugStream() << "Created movement start anim for " << 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->debugStream() << "Created reverse anim for " << 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(); } CShootingAnimation::CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool _catapult, int _catapultDmg) : CAttackAnimation(_owner, attacker, _dest, _attacked), catapultDamage(_catapultDmg) { logAnim->debugStream() << "Created shooting anim for " << 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->position, attackedStack->position, owner->creDir[attackingStack->ID], attackingStack->doubleWide(), owner->creDir[attackedStack->ID])) { owner->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->position, 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; // 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->position < 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 = abs((destPos.x - spi.x) / animSpeed); spi.dx = animSpeed; spi.dy = 0; SDL_Surface * img = owner->idToProjectile[spi.creID]->ourImages[0].bitmap; // Add explosion anim Point animPos(destPos.x - 126 + img->w / 2, destPos.y - 105 + img->h / 2); owner->addNewAnim( new CSpellEffectAnimation(owner, catapultDamage ? "SGEXPL.DEF" : "CSGRCK.DEF", animPos.x, animPos.y)); } auto & angles = shooterInfo->animation.missleFrameAngles; double pi = boost::math::constants::pi(); // 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[spi.creID]->ourImages.size()); 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) //upper shot group = CCreatureAnim::SHOOT_UP; else if(projectileAngle < -straightAngle) //lower shot 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; } CSpellEffectAnimation::CSpellEffectAnimation(CBattleInterface * _owner, ui32 _effect, BattleHex _destTile, int _dx, int _dy, bool _Vflip, bool _alignToBottom) :CBattleAnimation(_owner), effect(_effect), destTile(_destTile), customAnim(""), x(-1), y(-1), dx(_dx), dy(_dy), Vflip(_Vflip), alignToBottom(_alignToBottom) { logAnim->debugStream() << "Created spell anim for effect #" << effect; } CSpellEffectAnimation::CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx, int _dy, bool _Vflip, bool _alignToBottom) :CBattleAnimation(_owner), effect(-1), destTile(BattleHex::INVALID), customAnim(_customAnim), x(_x), y(_y), dx(_dx), dy(_dy), Vflip(_Vflip), alignToBottom(_alignToBottom) { logAnim->debugStream() << "Created spell anim for " << customAnim; } CSpellEffectAnimation::CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip, bool _alignToBottom) :CBattleAnimation(_owner), effect(-1), destTile(_destTile), customAnim(_customAnim), x(-1), y(-1), dx(0), dy(0), Vflip(_Vflip), alignToBottom(_alignToBottom) { logAnim->debugStream() << "Created spell anim for " << customAnim; } bool CSpellEffectAnimation::init() { if(!isEarliest(true)) return false; if(customAnim.empty() && effect != ui32(-1) && !graphics->battleACToDef[effect].empty()) { customAnim = graphics->battleACToDef[effect][0]; } if(customAnim.empty()) { endAnim(); return false; } const bool areaEffect = (!destTile.isValid() && x == -1 && y == -1); if(areaEffect) //f.e. armageddon { CDefHandler * anim = CDefHandler::giveDef(customAnim); for(int i=0; i * anim->width < owner->pos.w ; ++i) { for(int j=0; j * anim->height < owner->pos.h ; ++j) { BattleEffect be; be.effectID = ID; be.anim = CDefHandler::giveDef(customAnim); if (Vflip) { for (auto & elem : be.anim->ourImages) { CSDL_Ext::VflipSurf(elem.bitmap); } } be.currentFrame = 0; be.maxFrame = be.anim->ourImages.size(); be.x = i * anim->width + owner->pos.x; be.y = j * anim->height + owner->pos.y; be.position = BattleHex::INVALID; owner->battleEffects.push_back(be); } } delete anim; } else // Effects targeted at a specific creature/hex. { const CStack* destStack = owner->getCurrentPlayerInterface()->cb->battleGetStackByPos(destTile, false); Rect &tilePos = owner->bfield[destTile]->pos; BattleEffect be; be.effectID = ID; be.anim = CDefHandler::giveDef(customAnim); if (Vflip) { for (auto & elem : be.anim->ourImages) { CSDL_Ext::VflipSurf(elem.bitmap); } } be.currentFrame = 0; be.maxFrame = be.anim->ourImages.size(); //todo: lightning anim frame count override // if(effect == 1) // be.maxFrame = 3; if(x == -1) { be.x = tilePos.x + tilePos.w/2 - be.anim->width/2; } else { be.x = x; } if(y == -1) { if(alignToBottom) be.y = tilePos.y + tilePos.h - be.anim->height; else be.y = tilePos.y - be.anim->height/2; } else { be.y = y; } // Correction for 2-hex creatures. if (destStack != nullptr && destStack->doubleWide()) be.x += (destStack->attackerOwned ? -1 : 1)*tilePos.w/2; //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 CSpellEffectAnimation::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.maxFrame) { endAnim(); break; } else { elem.x += dx; elem.y += dy; } } } } void CSpellEffectAnimation::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) { delete elem->anim; owner->battleEffects.erase(elem); } delete this; } vcmi-0.98/client/battle/CBattleAnimations.h000066400000000000000000000162141250671757600206770ustar00rootroot00000000000000#pragma once #include "../../lib/BattleHex.h" #include "../widgets/Images.h" class CBattleInterface; class CStack; class CCreatureAnimation; struct CatapultProjectileInfo; struct StackAttackedInfo; /* * 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 * */ /// 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: CCreatureAnimation * 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(); void endAnim(); 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(); void nextFrame(); void endAnim(); CDefenceAnimation(StackAttackedInfo _attackedInfo, CBattleInterface * _owner); virtual ~CDefenceAnimation(){}; }; class CDummyAnimation : public CBattleAnimation { private: int counter; int howMany; public: bool init(); void nextFrame(); void endAnim(); CDummyAnimation(CBattleInterface * _owner, int howManyFrames); virtual ~CDummyAnimation(){} }; /// Hand-to-hand attack class CMeleeAttackAnimation : public CAttackAnimation { public: bool init(); void endAnim(); 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(); void nextFrame(); void endAnim(); 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(); void endAnim(); CMovementEndAnimation(CBattleInterface * _owner, const CStack * _stack, BattleHex destTile); virtual ~CMovementEndAnimation(){}; }; /// Move start animation of a creature class CMovementStartAnimation : public CBattleStackAnimation { public: bool init(); void endAnim(); 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(); static void rotateStack(CBattleInterface * owner, const CStack * stack, BattleHex hex); void setupSecondPart(); void endAnim(); 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 }; /// Shooting attack class CShootingAnimation : public CAttackAnimation { private: int catapultDamage; public: bool init(); void nextFrame(); void endAnim(); //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(){}; }; /// This class manages a spell effect animation class CSpellEffectAnimation : public CBattleAnimation { private: ui32 effect; BattleHex destTile; std::string customAnim; int x, y, dx, dy; bool Vflip; bool alignToBottom; public: bool init(); void nextFrame(); void endAnim(); CSpellEffectAnimation(CBattleInterface * _owner, ui32 _effect, BattleHex _destTile, int _dx = 0, int _dy = 0, bool _Vflip = false, bool _alignToBottom = false); CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx = 0, int _dy = 0, bool _Vflip = false, bool _alignToBottom = false); CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip = false, bool _alignToBottom = false); virtual ~CSpellEffectAnimation(){}; }; vcmi-0.98/client/battle/CBattleInterface.cpp000066400000000000000000003411521250671757600210320ustar00rootroot00000000000000#include "StdInc.h" #include "CBattleInterface.h" #include "CBattleAnimations.h" #include "CBattleInterfaceClasses.h" #include "CCreatureAnimation.h" #include "../CBitmapHandler.h" #include "../CDefHandler.h" #include "../CGameInfo.h" #include "../CMessage.h" #include "../CMT.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../CVideoHandler.h" #include "../Graphics.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/BattleState.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" /* * 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 * */ CondSh CBattleInterface::animsAreDisplayed; static void onAnimationFinished(const CStack *stack, CCreatureAnimation * anim) { if (anim->isIdle()) { const CCreature *creature = stack->getCreature(); if (anim->framesInGroup(CCreatureAnim::MOUSEON) > 0) { if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets * 10) anim->playOnce(CCreatureAnim::MOUSEON); else anim->setType(CCreatureAnim::HOLDING); } else { anim->setType(CCreatureAnim::HOLDING); } } // always reset callback anim->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, shared_ptr att, shared_ptr defen) : background(nullptr), queue(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), spellSelMode(NO_LOCATION), spellToCast(nullptr), sp(nullptr), siegeH(nullptr), attackerInt(att), defenderInt(defen), curInt(att), animIDhelper(0), givenCommand(nullptr), myTurn(false), resWindow(nullptr), moveStarted(false), moveSoundHander(-1), bresult(nullptr) { OBJ_CONSTRUCTION; 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 = new CondSh(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 = screen->h < 700; queue = new CStackQueue(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->pos.x = pos.x; // queue->pos.y = pos.y - queue->pos.h; // pos.h += queue->pos.h; // center(); } queue->update(); //preparing siege info const CGTownInstance * town = curInt->cb->battleGetDefendedTown(); if(town && town->hasFort()) { siegeH = new SiegeHelper(town, this); } curInt->battleInt = this; //initializing armies this->army1 = army1; this->army2 = army2; std::vector stacks = curInt->cb->battleGetAllStacks(true); for(const CStack *s : stacks) { newStack(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->errorStream() << bfieldType << " is not valid battlefield type index!"; else if(graphics->battleBacks[bfieldType].empty()) logGlobal->errorStream() << bfieldType << " battlefield type does not have any backgrounds!"; else { const std::string bgName = *RandomGeneratorUtil::nextItem(graphics->battleBacks[bfieldType], CRandomGenerator::getDefault()); background = BitmapHandler::loadBitmap(bgName, false); } } //preparing menu background //graphics->blueToPlayersAdv(menu, hero1->tempOwner); //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); ////blitting menu background and terrain // blitAt(background, pos.x, pos.y); // blitAt(menu, pos.x, 556 + pos.y); //preparing buttons and console bOptions = new CButton (Point( 3, 561), "icm003.def", CGI->generaltexth->zelp[381], std::bind(&CBattleInterface::bOptionsf,this), SDLK_o); bSurrender = new CButton (Point( 54, 561), "icm001.def", CGI->generaltexth->zelp[379], std::bind(&CBattleInterface::bSurrenderf,this), SDLK_s); bFlee = new CButton (Point(105, 561), "icm002.def", CGI->generaltexth->zelp[380], std::bind(&CBattleInterface::bFleef,this), SDLK_r); bAutofight = new CButton (Point(157, 561), "icm004.def", CGI->generaltexth->zelp[382], std::bind(&CBattleInterface::bAutofightf,this), SDLK_a); bSpell = new CButton (Point(645, 561), "icm005.def", CGI->generaltexth->zelp[385], std::bind(&CBattleInterface::bSpellf,this), SDLK_c); bWait = new CButton (Point(696, 561), "icm006.def", CGI->generaltexth->zelp[386], std::bind(&CBattleInterface::bWaitf,this), SDLK_w); bDefence = new CButton (Point(747, 561), "icm007.def", CGI->generaltexth->zelp[387], std::bind(&CBattleInterface::bDefencef,this), SDLK_d); bDefence->assignedKeys.insert(SDLK_SPACE); bConsoleUp = new CButton (Point(624, 561), "ComSlide.def", std::make_pair("", ""), std::bind(&CBattleInterface::bConsoleUpf,this), SDLK_UP); bConsoleDown = new CButton (Point(624, 580), "ComSlide.def", std::make_pair("", ""), std::bind(&CBattleInterface::bConsoleDownf,this), SDLK_DOWN); bConsoleDown->setImageOrder(2, 3, 4, 5); console = new CBattleConsole(); console->pos.x += 211; console->pos.y += 560; console->pos.w = 406; console->pos.h = 38; if(tacticsMode) { btactNext = new CButton(Point(213, 560), "icm011.def", std::make_pair("", ""), [&]{ bTacticNextStack(nullptr);}, SDLK_SPACE); btactEnd = new CButton(Point(419, 560), "icm012.def", std::make_pair("", ""), [&]{ bEndTacticPhase();}, SDLK_RETURN); menu = BitmapHandler::loadBitmap("COPLACBR.BMP"); } else { menu = BitmapHandler::loadBitmap("CBAR.BMP"); btactEnd = btactNext = nullptr; } graphics->blueToPlayersAdv(menu, curInt->playerID); //loading hero animations if(hero1) // attacking hero { std::string battleImage; if ( hero1->sex ) battleImage = hero1->type->heroClass->imageBattleFemale; else battleImage = hero1->type->heroClass->imageBattleMale; attackingHero = new CBattleHero(battleImage, false, hero1->tempOwner, hero1->tempOwner == curInt->playerID ? hero1 : nullptr, this); attackingHero->pos = genRect(attackingHero->dh->ourImages[0].bitmap->h, attackingHero->dh->ourImages[0].bitmap->w, pos.x - 43, pos.y - 19); } else { attackingHero = nullptr; } if(hero2) // defending hero { std::string battleImage; if ( hero2->sex ) battleImage = hero2->type->heroClass->imageBattleFemale; else battleImage = hero2->type->heroClass->imageBattleMale; defendingHero = new CBattleHero(battleImage, true, hero2->tempOwner, hero2->tempOwner == curInt->playerID ? hero2 : nullptr, this); defendingHero->pos = genRect(defendingHero->dh->ourImages[0].bitmap->h, defendingHero->dh->ourImages[0].bitmap->w, pos.x + 693, pos.y - 19); } else { defendingHero = nullptr; } //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 = new CClickableHex(); 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->position >= 0) //turrets have position < 0 bfield[s->position]->accessible = false; //loading projectiles for units for(const CStack *s : stacks) { if(s->getCreature()->isShooting()) { CDefHandler *&projectile = idToProjectile[s->getCreature()->idNumber]; const CCreature * creature;//creature whose shots should be loaded if (s->getCreature()->idNumber == CreatureID::ARROW_TOWERS) creature = CGI->creh->creatures[siegeH->town->town->clientInfo.siegeShooter]; else creature = s->getCreature(); projectile = CDefHandler::giveDef(creature->animation.projectileImageName); for(auto & elem : projectile->ourImages) //alpha transforming { CSDL_Ext::alphaTransform(elem.bitmap); } } } //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) { const int ID = elem->ID; if(elem->obstacleType == CObstacleInstance::USUAL) { idToObstacle[ID] = CDefHandler::giveDef(elem->getInfo().defName); for(auto & _n : idToObstacle[ID]->ourImages) { CSDL_Ext::setDefaultColorKey(_n.bitmap); } } else if(elem->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { idToAbsoluteObstacle[ID] = BitmapHandler::loadBitmap(elem->getInfo().defName); } } quicksand = CDefHandler::giveDef("C17SPE1.DEF"); landMine = CDefHandler::giveDef("C09SPF1.DEF"); fireWall = CDefHandler::giveDef("C07SPF61"); bigForceField[0] = CDefHandler::giveDef("C15SPE10.DEF"); bigForceField[1] = CDefHandler::giveDef("C15SPE7.DEF"); smallForceField[0] = CDefHandler::giveDef("C15SPE1.DEF"); smallForceField[1] = CDefHandler::giveDef("C15SPE4.DEF"); for(auto hex : bfield) addChild(hex); 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); } CBattleInterface::~CBattleInterface() { curInt->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); delete bOptions; delete bSurrender; delete bFlee; delete bAutofight; delete bSpell; delete bWait; delete bDefence; for(auto hex : bfield) delete hex; delete bConsoleUp; delete bConsoleDown; delete console; delete givenCommand; delete attackingHero; delete defendingHero; delete queue; SDL_FreeSurface(cellBorder); SDL_FreeSurface(cellShade); for(auto & elem : creAnims) delete elem.second; for(auto & elem : idToProjectile) delete elem.second; for(auto & elem : idToObstacle) delete elem.second; delete quicksand; delete landMine; delete fireWall; delete smallForceField[0]; delete smallForceField[1]; delete bigForceField[0]; delete bigForceField[1]; 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); } } 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_ESCAPE && spellDestSelectMode) { endCastingSpell(); } } void CBattleInterface::mouseMoved(const SDL_MouseMotionEvent &sEvent) { auto hexItr = std::find_if(bfield.begin(), bfield.end(), [](const CClickableHex *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; const double hexMidY = hoveredHex.pos.y + hoveredHex.pos.h/2; 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->errorStream() << "Error: for hex " << myNumber << " cannot find a hex to attack from!"; 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 && spellDestSelectMode) { 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(); auto optionsWin = new CBattleOptionsWindow(tempRect, this); GH.pushInt(optionsWin); } 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(); }, 0, false); } } 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, 0, false); //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(Battle::RETREAT,0,0); 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(Battle::SURRENDER,0,0); 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->traceStream() << "Stopping the autofight..."; } else { curInt->isAutoFightOn = true; blockUI(true); auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].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; CCS->curh->changeGraphic(ECursor::ADVENTURE,0); if(!myTurn) return; auto myHero = currentHero(); ESpellCastProblem::ESpellCastProblem spellCastProblem; if (curInt->cb->battleCanCastSpell(&spellCastProblem)) { auto spellWindow = new CSpellWindow(genRect(595, 620, (screen->w - 620)/2, (screen->h - 595)/2), myHero, curInt.get()); GH.pushInt(spellWindow); } else if(spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED) { //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(Battle::WAIT,0,activeStack->ID); } void CBattleInterface::bDefencef() { if(spellDestSelectMode) //we are casting a spell return; if(activeStack != nullptr) giveCommand(Battle::DEFEND,0,activeStack->ID); } 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::newStack(const CStack * stack) { creDir[stack->ID] = stack->attackerOwned; // must be set before getting stack position Point coords = CClickableHex::getXYUnitAnim(stack->position, stack, this); if(stack->position < 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->position) { 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); } void CBattleInterface::stackRemoved(int stackID) { delete creAnims[stackID]; creAnims.erase(stackID); creDir.erase(stackID); //FIXME: what if currently removed stack is active one (Sacrifice)? redrawBackgroundWithHexes(activeStack); queue->update(); } void CBattleInterface::stackActivated(const CStack * stack) //TODO: check it all before game state is changed due to abilities { //givenCommand = nullptr; stackToActivate = stack; waitForAnims(); //if(pendingAnims.size() == 0) 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) { 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->position); //TODO: play reverse death animation CCS->soundh->playSound(soundBase::RESURECT); } } waitForAnims(); int targets = 0, killed = 0, damage = 0; for(auto & attackedInfo : attackedInfos) { ++targets; killed += attackedInfo.amountKilled; damage += attackedInfo.dmg; } for(auto & attackedInfo : attackedInfos) { if (attackedInfo.rebirth) creAnims[attackedInfo.defender->ID]->setType(CCreatureAnim::HOLDING); if (attackedInfo.cloneKilled) stackRemoved(attackedInfo.defender->ID); } /* if (attackedInfos.front().cloneKilled) //FIXME: cloned stack is already removed return;*/ if (targets > 1) printConsoleAttacked(attackedInfos.front().defender, damage, killed, attackedInfos.front().attacker, true); //creatures perish else printConsoleAttacked(attackedInfos.front().defender, damage, killed, attackedInfos.front().attacker, false); } 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]); //unlock spellbook //bSpell->block(!curInt->cb->battleCanCastSpell()); //don't unlock spellbook - this should be done when we have axctive creature } void CBattleInterface::giveCommand(Battle::ActionType action, BattleHex tile, ui32 stackID, si32 additional, si32 selected) { const CStack *stack = curInt->cb->battleGetStackByID(stackID); if(!stack && action != Battle::HERO_SPELL && action != Battle::RETREAT && action != Battle::SURRENDER) { return; } if(stack && stack != activeStack) logGlobal->warnStream() << "Warning: giving an order to a non-active stack?"; auto ba = new BattleAction(); //is deleted in CPlayerInterface::activeStack() ba->side = defendingHeroInstance ? (curInt->playerID == defendingHeroInstance->tempOwner) : false; ba->actionType = action; ba->destinationTile = tile; ba->stackNumber = stackID; ba->additionalInfo = additional; ba->selectedStack = selected; //some basic validations switch(action) { case Battle::WALK_AND_ATTACK: assert(curInt->cb->battleGetStackByPos(additional)); //stack to attack must exist case Battle::WALK: case Battle::SHOOT: case Battle::CATAPULT: assert(tile < GameConstants::BFIELD_SIZE); break; } if(!tacticsMode) { logGlobal->traceStream() << "Setting command for " << (stack ? stack->nodeName() : "hero"); myTurn = false; setActiveStack(nullptr); givenCommand->setn(ba); } else { curInt->cb->battleMakeTacticAction(ba); vstd::clear_pointer(ba); 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->attackerOwned) { 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 CSpellEffectAnimation(this, "SGEXPL.DEF", destPos.x, destPos.y)); } } waitForAnims(); for(auto attackInfo : ca.attackedParts) { SDL_FreeSurface(siegeH->walls[attackInfo.attackedPart + 2]); siegeH->walls[attackInfo.attackedPart + 2] = BitmapHandler::loadBitmap( siegeH->getSiegeName(attackInfo.attackedPart + 2, curInt->cb->battleGetWallState(attackInfo.attackedPart))); } } void CBattleInterface::battleFinished(const BattleResult& br) { bresult = &br; { auto unlockPim = vstd::makeUnlockGuard(*LOCPLINT->pim); animsAreDisplayed.waitUntil(false); } displayBattleFinished(); setActiveStack(nullptr); } void CBattleInterface::displayBattleFinished() { CCS->curh->changeGraphic(ECursor::ADVENTURE,0); SDL_Rect temp_rect = genRect(561, 470, (screen->w - 800)/2 + 165, (screen->h - 600)/2 + 19); resWindow = new CBattleResultWindow(*bresult, temp_rect, *this->curInt); GH.pushInt(resWindow); } void CBattleInterface::spellCast( const BattleSpellCast * sc ) { const SpellID spellID(sc->id); const CSpell &spell = * spellID.toSpell(); const std::string & spellName = spell.name; const std::string& castSoundPath = spell.getCastSound(); std::string casterName("Something"); if(sc->castedByHero) casterName = curInt->cb->battleGetHeroInfo(sc->side).name; if(!castSoundPath.empty()) CCS->soundh->playSound(castSoundPath); Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos; //hero position by default { const auto casterStackID = sc->casterStack; if(casterStackID > 0) { const CStack * casterStack = curInt->cb->battleGetStackByID(casterStackID); if(casterStack != nullptr) { casterName = casterStack->type->namePl; srccoord = CClickableHex::getXYUnitAnim(casterStack->position, casterStack, this); srccoord.x += 250; srccoord.y += 240; } } } //TODO: play custom 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()) { //displaying animation CDefEssential * animDef = CDefHandler::giveDefEss(animToDisplay); 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 - animDef->ourImages[0].bitmap->w)/steps; int dy = (destcoord.y - srccoord.y - animDef->ourImages[0].bitmap->h)/steps; delete animDef; addNewAnim(new CSpellEffectAnimation(this, animToDisplay, srccoord.x, srccoord.y, dx, dy, Vflip)); } } waitForAnims(); displaySpellHit(spellID, sc->tile); //queuing affect /resist animation for (auto & elem : sc->affectedCres) { BattleHex position = curInt->cb->battleGetStackByID(elem, false)->position; if(vstd::contains(sc->resisted,elem)) displayEffect(78, position); else displaySpellEffect(spellID, position); } switch(sc->id) { case SpellID::SUMMON_FIRE_ELEMENTAL: case SpellID::SUMMON_EARTH_ELEMENTAL: case SpellID::SUMMON_WATER_ELEMENTAL: case SpellID::SUMMON_AIR_ELEMENTAL: case SpellID::CLONE: case SpellID::REMOVE_OBSTACLE: addNewAnim(new CDummyAnimation(this, 2)); //interface won't return until animation is played. TODO: make it smarter? break; } //switch(sc->id) //displaying message in console bool customSpell = false; if(sc->affectedCres.size() == 1) { const CStack * attackedStack = curInt->cb->battleGetStackByID(*sc->affectedCres.begin(), false); const std::string attackedName = attackedStack->getName(); const std::string attackedNameSing = attackedStack->getCreature()->nameSing; const std::string attackedNamePl = attackedStack->getCreature()->namePl; std::string text = CGI->generaltexth->allTexts[195]; if(sc->castedByHero) { boost::algorithm::replace_first(text, "%s", casterName); boost::algorithm::replace_first(text, "%s", spellName); boost::algorithm::replace_first(text, "%s", attackedNamePl); //target } else { auto getPluralText = [attackedStack](const int baseTextID) -> std::string { return CGI->generaltexth->allTexts[(attackedStack->count > 1 ? baseTextID+1 : baseTextID)]; }; bool plural = false; //add singular / plural form of creature text if this is true int textID = 0; switch(sc->id) { case SpellID::STONE_GAZE: customSpell = true; plural = true; textID = 558; break; case SpellID::POISON: customSpell = true; plural = true; textID = 561; break; case SpellID::BIND: customSpell = true; text = CGI->generaltexth->allTexts[560]; boost::algorithm::replace_first(text, "%s", attackedNamePl); break;//Roots and vines bind the %s to the ground! case SpellID::DISEASE: customSpell = true; plural = true; textID = 553; break; case SpellID::PARALYZE: customSpell = true; plural = true; textID = 563; break; case SpellID::AGE: { customSpell = true; text = getPluralText(551); boost::algorithm::replace_first(text, "%s", attackedName); //The %s shrivel with age, and lose %d hit points." TBonusListPtr bl = attackedStack->getBonuses(Selector::type(Bonus::STACK_HEALTH)); bl->remove_if(Selector::source(Bonus::SPELL_EFFECT, SpellID::AGE)); boost::algorithm::replace_first(text, "%d", boost::lexical_cast(bl->totalValue()/2)); } break; case SpellID::THUNDERBOLT: text = CGI->generaltexth->allTexts[367]; boost::algorithm::replace_first(text, "%s", attackedNamePl); console->addText(text); text = CGI->generaltexth->allTexts[343].substr(1, CGI->generaltexth->allTexts[343].size() - 1); //Does %d points of damage. boost::algorithm::replace_first(text, "%d", boost::lexical_cast(sc->dmgToDisplay)); //no more text afterwards console->addText(text); customSpell = true; text = ""; //yeah, it's a terrible mess break; case SpellID::DISPEL_HELPFUL_SPELLS: text = CGI->generaltexth->allTexts[555]; boost::algorithm::replace_first(text, "%s", attackedNamePl); customSpell = true; break; case SpellID::DEATH_STARE: customSpell = true; if (sc->dmgToDisplay) { if (sc->dmgToDisplay > 1) { text = CGI->generaltexth->allTexts[119]; //%d %s die under the terrible gaze of the %s. boost::algorithm::replace_first(text, "%d", boost::lexical_cast(sc->dmgToDisplay)); boost::algorithm::replace_first(text, "%s", attackedNamePl); } else { text = CGI->generaltexth->allTexts[118]; //One %s dies under the terrible gaze of the %s. boost::algorithm::replace_first(text, "%s", attackedNameSing); } boost::algorithm::replace_first(text, "%s", casterName); //casting stack } else text = ""; break; default: text = CGI->generaltexth->allTexts[565]; //The %s casts %s boost::algorithm::replace_first(text, "%s", casterName); //casting stack } if (plural) { text = getPluralText(textID); boost::algorithm::replace_first(text, "%s", attackedName); } } if (!customSpell && !sc->dmgToDisplay) boost::algorithm::replace_first(text, "%s", spellName); //simple spell name if (text.size()) console->addText(text); } else { std::string text = CGI->generaltexth->allTexts[196]; boost::algorithm::replace_first(text, "%s", casterName); boost::algorithm::replace_first(text, "%s", spellName); console->addText(text); } if(sc->dmgToDisplay && !customSpell) { std::string dmgInfo = CGI->generaltexth->allTexts[376]; boost::algorithm::replace_first(dmgInfo, "%s", spellName); //simple spell name boost::algorithm::replace_first(dmgInfo, "%d", boost::lexical_cast(sc->dmgToDisplay)); console->addText(dmgInfo); //todo: casualties (?) } waitForAnims(); //mana absorption if(sc->manaGained > 0) { Point leftHero = Point(15, 30) + pos; Point rightHero = Point(755, 30) + pos; addNewAnim(new CSpellEffectAnimation(this, sc->side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero.x, leftHero.y, 0, 0, false)); addNewAnim(new CSpellEffectAnimation(this, sc->side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero.x, rightHero.y, 0, 0, false)); } } void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) { if (sse.effect.back().sid == -1 && sse.stacks.size() == 1 && sse.effect.size() == 2) { const Bonus & bns = sse.effect.front(); if (bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL) { //defensive stance const CStack * stack = LOCPLINT->cb->battleGetStackByID(*sse.stacks.begin()); int txtid = 120; if(stack->count != 1) txtid++; //move to plural text BonusList defenseBonuses = *(stack->getBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE))); defenseBonuses.remove_if(Selector::durationType(Bonus::STACK_GETS_TURN)); //remove bonuses gained from defensive stance int val = stack->Defense() - defenseBonuses.totalValue(); auto txt = boost::format (CGI->generaltexth->allTexts[txtid]) % ((stack->count != 1) ? stack->getCreature()->namePl : stack->getCreature()->nameSing) % val; console->addText(boost::to_string(txt)); } } if (activeStack != nullptr) //it can be -1 when a creature casts effect { redrawBackgroundWithHexes(activeStack); } } void CBattleInterface::castThisSpell(SpellID spellID) { auto ba = new BattleAction; ba->actionType = Battle::HERO_SPELL; ba->additionalInfo = spellID; //spell number ba->destinationTile = -1; ba->stackNumber = (attackingHeroInstance->tempOwner == curInt->playerID) ? -1 : -2; ba->side = defendingHeroInstance ? (curInt->playerID == defendingHeroInstance->tempOwner) : false; spellToCast = ba; spellDestSelectMode = true; creatureCasting = false; //choosing possible tragets const CGHeroInstance * castingHero = (attackingHeroInstance->tempOwner == curInt->playerID) ? attackingHeroInstance : defendingHeroInstance; assert(castingHero); // code below assumes non-null hero sp = CGI->spellh->objects[spellID]; spellSelMode = ANY_LOCATION; const CSpell::TargetInfo ti = sp->getTargetInfo(castingHero->getSpellSchoolLevel(sp)); if(ti.massive || ti.type == CSpell::NO_TARGET) spellSelMode = NO_LOCATION; else if(ti.type == CSpell::LOCATION && ti.clearAffected) { spellSelMode = FREE_LOCATION; } else if(ti.type == CSpell::CREATURE) { if(ti.smart) spellSelMode = selectionTypeByPositiveness(*sp); else spellSelMode = ANY_CREATURE; } else if(ti.type == CSpell::OBSTACLE) { spellSelMode = OBSTACLE; } if (spellSelMode == NO_LOCATION) //user does not have to select location { spellToCast->destinationTile = -1; curInt->cb->battleMakeAction(spellToCast); endCastingSpell(); } else { possibleActions.clear(); possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment GH.fakeMouseMove();//update cursor } } void CBattleInterface::displayEffect(ui32 effect, int destTile, bool areaEffect) { //todo: recheck areaEffect usage addNewAnim(new CSpellEffectAnimation(this, effect, destTile, 0, 0, false)); } void CBattleInterface::displaySpellEffect(SpellID spellID, BattleHex destinationTile, bool areaEffect) { const CSpell * spell = spellID.toSpell(); if(spell == nullptr) return; for(const CSpell::TAnimation & animation : spell->animationInfo.affect) { addNewAnim(new CSpellEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM)); } } void CBattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTile, bool areaEffect) { const CSpell * spell = spellID.toSpell(); if(spell == nullptr) return; for(const CSpell::TAnimation & animation : spell->animationInfo.hit) { addNewAnim(new CSpellEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM)); } } void CBattleInterface::battleTriggerEffect(const BattleTriggerEffect & bte) { const CStack * stack = curInt->cb->battleGetStackByID(bte.stackID); //don't show animation when no HP is regenerated switch (bte.effect) { case Bonus::HP_REGENERATION: displayEffect(74, stack->position); CCS->soundh->playSound(soundBase::REGENER); break; case Bonus::MANA_DRAIN: displayEffect(77, stack->position); CCS->soundh->playSound(soundBase::MANADRAI); break; case Bonus::POISON: displayEffect(67, stack->position); CCS->soundh->playSound(soundBase::POISON); break; case Bonus::FEAR: displayEffect(15, stack->position); 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->position); 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 { 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 Bonus *spellcaster = s->getBonusLocalFirst(Selector::type(Bonus::SPELLCASTER)), *randomSpellcaster = s->getBonusLocalFirst(Selector::type(Bonus::RANDOM_SPELLCASTER)); if (s->casts && (spellcaster || randomSpellcaster)) { stackCanCastSpell = true; if(randomSpellcaster) creatureSpellToCast = -1; //spell will be set later on cast creatureSpellToCast = curInt->cb->battleGetRandomStackSpell(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? } else { stackCanCastSpell = false; creatureSpellToCast = -1; } getPossibleActionsForStack (s); GH.fakeMouseMove(); } void CBattleInterface::endCastingSpell() { assert(spellDestSelectMode); delete spellToCast; spellToCast = nullptr; sp = nullptr; spellDestSelectMode = false; CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); if (activeStack) { getPossibleActionsForStack (activeStack); //restore actions after they were cleared myTurn = true; } } void CBattleInterface::getPossibleActionsForStack(const CStack * stack) { possibleActions.clear(); if (tacticsMode) { possibleActions.push_back(MOVE_TACTICS); possibleActions.push_back(CHOOSE_TACTICS_STACK); } else { //first action will be prioritized over later ones if (stack->casts) //TODO: check for battlefield effects that prevent casting? { if (stack->hasBonusOfType (Bonus::SPELLCASTER)) { //TODO: poll possible spells const CSpell * spell; BonusList spellBonuses = *stack->getBonuses (Selector::type(Bonus::SPELLCASTER)); for (Bonus * spellBonus : spellBonuses) { spell = CGI->spellh->objects[spellBonus->subtype]; switch (spellBonus->subtype) { case SpellID::REMOVE_OBSTACLE: possibleActions.push_back (OBSTACLE); break; default: possibleActions.push_back (selectionTypeByPositiveness (*spell)); break; } } std::sort(possibleActions.begin(), possibleActions.end()); auto it = std::unique (possibleActions.begin(), possibleActions.end()); possibleActions.erase (it, possibleActions.end()); } 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->shots && stack->hasBonusOfType (Bonus::SHOOTER)) 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()) //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); } } 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 { boost::format txt = boost::format (CGI->generaltexth->allTexts[attacker->count > 1 ? 377 : 376]) % (attacker->count > 1 ? attacker->getCreature()->namePl : attacker->getCreature()->nameSing) % dmg; formattedText.append(boost::to_string(txt)); } 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 == Battle::HERO_SPELL) { if(action->side) defendingHero->setPhase(0); else attackingHero->setPhase(0); } if(stack && action->actionType == Battle::WALK && !creAnims[action->stackNumber]->isIdle()) //walk or walk & attack { pendingAnims.push_back(std::make_pair(new CMovementEndAnimation(this, stack, action->destinationTile), false)); } //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] != bool(s->attackerOwned) && s->alive() && creAnims[s->ID]->isIdle()) { addNewAnim(new CReverseAnimation(this, s, s->position, false)); } } queue->update(); if(tacticsMode) //stack ended movement in tactics phase -> select the next one bTacticNextStack(stack); if( action->actionType == Battle::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->warnStream() << "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) { ESpellCastProblem::ESpellCastProblem spellcastingProblem; bool canCastSpells = curInt->cb->battleCanCastSpell(&spellcastingProblem); 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); } //if magic is blocked, we leave button active, so the message can be displayed (cf bug #97) bSpell->block(on || (!canCastSpells && spellcastingProblem != ESpellCastProblem::MAGIC_IS_BLOCKED)); bWait->block(on || tacticsMode || !canWait); bDefence->block(on || tacticsMode); } void CBattleInterface::startAction(const BattleAction* action) { //setActiveStack(nullptr); setHoveredStack(nullptr); blockUI(true); if(action->actionType == Battle::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 == Battle::HERO_SPELL); //only cast spell is valid action without acting stack number } if(action->actionType == Battle::WALK || (action->actionType == Battle::WALK_AND_ATTACK && action->destinationTile != stack->position)) { 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 == Battle::HERO_SPELL) //when hero casts spell { if(action->side) defendingHero->setPhase(4); else attackingHero->setPhase(4); return; } if(!stack) { logGlobal->errorStream()<<"Something wrong with stackNumber in actionStarted. Stack number: "<stackNumber; return; } int txtid = 0; switch(action->actionType) { case Battle::WAIT: txtid = 136; break; case Battle::BAD_MORALE: txtid = -34; //negative -> no separate singular/plural form displayEffect(30,stack->position); CCS->soundh->playSound(soundBase::BADMRLE); break; } if(txtid > 0 && stack->count != 1) txtid++; //move to plural text else if(txtid < 0) txtid = -txtid; if(txtid) { std::string name = (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str(); console->addText((boost::format(CGI->generaltexth->allTexts[txtid].c_str()) % name).str()); } //displaying special abilities switch (action->actionType) { case Battle::STACK_HEAL: displayEffect(74, action->destinationTile); CCS->soundh->playSound(soundBase::REGENER); break; } } void CBattleInterface::waitForAnims() { auto unlockPim = vstd::makeUnlockGuard(*LOCPLINT->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 /*= nullptr*/) { if(!current) current = activeStack; //no switching stacks when the current one is moving waitForAnims(); TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE); stacksOfMine.erase(std::remove_if(stacksOfMine.begin(), stacksOfMine.end(), &immobile), stacksOfMine.end()); auto it = vstd::find(stacksOfMine, current); if(it != stacksOfMine.end() && ++it != stacksOfMine.end()) stackActivated(*it); else stackActivated(stacksOfMine.front()); } CBattleInterface::PossibleActions CBattleInterface::selectionTypeByPositiveness(const CSpell & spell) { switch(spell.positiveness) { case CSpell::NEGATIVE : return HOSTILE_CREATURE_SPELL; case CSpell::NEUTRAL: return ANY_CREATURE; case CSpell::POSITIVE: return FRIENDLY_CREATURE_SPELL; } assert(0); return NO_LOCATION; //should never happen } 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, false); int shiftedDest = myNumber + (activeStack->attackerOwned ? 1 : -1); 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; const CStack * const sactive = activeStack; //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 (!sactive) 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(); 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 (sactive, myNumber)) legalAction = true; } break; } case ATTACK: case WALK_AND_ATTACK: case ATTACK_AND_RETURN: { if (shere && !ourStack && shere->alive()) { if (isTileAttackable(myNumber)) { 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 { creatureCasting = stackCanCastSpell && !spellDestSelectMode; //as isCastingPossibleHere is not called legalAction = true; } break; case HOSTILE_CREATURE_SPELL: if (shere && shere->alive() && !ourStack && isCastingPossibleHere (sactive, shere, myNumber)) legalAction = true; break; case FRIENDLY_CREATURE_SPELL: { if (isCastingPossibleHere (sactive, shere, myNumber)) //need to be called before sp is determined { bool rise = false; //TODO: can you imagine rising hostile creatures? sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : spellToCast->additionalInfo]; if (sp && sp->isRisingSpell()) rise = true; if (shere && (shere->alive() || rise) && ourStack) legalAction = true; } break; } case RANDOM_GENIE_SPELL: { if (shere && ourStack && shere != sactive) //only positive spells for other allied creatures { int spellID = curInt->cb->battleGetRandomStackSpell(shere, CBattleInfoCallback::RANDOM_GENIE); if (spellID > -1) { legalAction = true; } } } break; case OBSTACLE: if (isCastingPossibleHere (sactive, shere, myNumber)) legalAction = true; break; case TELEPORT: { ui8 skill = 0; if (creatureCasting) skill = sactive->valOfBonuses(Selector::typeSubtype(Bonus::SPELLCASTER, SpellID::TELEPORT)); else skill = getActiveHero()->getSpellSchoolLevel (CGI->spellh->objects[spellToCast->additionalInfo]); //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: { ui8 side = curInt->cb->battleGetMySide(); auto hero = curInt->cb->battleGetMyHero(); assert(!creatureCasting); //we assume hero casts this spell assert(hero); legalAction = true; bool hexesOutsideBattlefield = false; auto tilesThatMustBeClear = sp->rangeInHexes(myNumber, hero->getSpellSchoolLevel(sp), side, &hexesOutsideBattlefield); for(BattleHex hex : tilesThatMustBeClear) { if(curInt->cb->battleGetStackByPos(hex, true) || !!curInt->cb->battleGetObstacleOnPos(hex, false) || !hex.isAvailable()) { legalAction = false; notLegal = true; } } if(hexesOutsideBattlefield) { 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()) legalAction = true; break; } if (legalAction) localActions.push_back (action); else if (notLegal) 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 9they 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, false); int shiftedDest = myNumber + (activeStack->attackerOwned ? 1 : -1); if(vstd::contains(acc, myNumber)) giveCommand (Battle::WALK ,myNumber, activeStack->ID); else if(vstd::contains(acc, shiftedDest)) giveCommand (Battle::WALK, shiftedDest, activeStack->ID); } else { giveCommand (Battle::WALK, myNumber, activeStack->ID); } }; 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? realizeAction = [=] { BattleHex attackFromHex = fromWhichHexAttack(myNumber); if (attackFromHex >= 0) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) { giveCommand(Battle::WALK_AND_ATTACK, attackFromHex, activeStack->ID, myNumber); } }; std::string estDmgText = formatDmgRange(curInt->cb->battleEstimateDamage(sactive, 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(Battle::SHOOT, myNumber, activeStack->ID);}; std::string estDmgText = formatDmgRange(curInt->cb->battleEstimateDamage(sactive, shere)); //calculating estimated dmg //printing - Shoot %s (%d shots left, %s damage) consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % sactive->shots % estDmgText).str(); } break; case HOSTILE_CREATURE_SPELL: case FRIENDLY_CREATURE_SPELL: sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : spellToCast->additionalInfo]; //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->additionalInfo]; //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; spellToCast->selectedStack = shere->ID; //sacrificed creature is selected isCastingPossible = true; break; case FREE_LOCATION: //cursorFrame = ECursor::SPELLBOOK; 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(Battle::STACK_HEAL, myNumber, activeStack->ID); }; //command healing break; case RISE_DEMONS: cursorType = ECursor::SPELLBOOK; realizeAction = [=]{ giveCommand(Battle::DAEMON_SUMMONING, myNumber, activeStack->ID); }; break; case CATAPULT: cursorFrame = ECursor::COMBAT_SHOOT_CATAPULT; realizeAction = [=]{ giveCommand(Battle::CATAPULT, myNumber, activeStack->ID); }; break; case CREATURE_INFO: { cursorFrame = ECursor::COMBAT_QUERY; consoleMsg = (boost::format(CGI->generaltexth->allTexts[297]) % shere->getName()).str(); realizeAction = [=]{ GH.pushInt(new CStackWindow(shere, false)); }; break; } } } else //no possible valid action, display message { switch (illegalAction) { case HOSTILE_CREATURE_SPELL: case FRIENDLY_CREATURE_SPELL: 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 possibleActions.push_back (TELEPORT); spellToCast->selectedStack = selectedStack->ID; break; case SpellID::SACRIFICE: possibleActions.push_back (SACRIFICE); break; } } else { if(creatureCasting) { if (sp) { giveCommand(Battle::MONSTER_SPELL, myNumber, sactive->ID, creatureSpellToCast); } else //unknown random spell { giveCommand(Battle::MONSTER_SPELL, myNumber, sactive->ID, curInt->cb->battleGetRandomStackSpell(shere, CBattleInfoCallback::RANDOM_GENIE)); } } else { assert (sp); switch (sp->id.toEnum()) { case SpellID::SACRIFICE: spellToCast->destinationTile = selectedStack->position; //cast on first creature that will be resurrected break; default: spellToCast->destinationTile = myNumber; break; } curInt->cb->battleMakeAction(spellToCast); endCastingSpell(); } selectedStack = nullptr; } }; } //helper lambda that appropriately realizes action / sets cursor and tooltip auto realizeThingsToDo = [&]() { 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(""); } }; realizeThingsToDo(); } 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->additionalInfo; sp = nullptr; if (spellID >= 0) sp = CGI->spellh->objects[spellID]; if (sp) { if (creatureCasting) isCastingPossible = (curInt->cb->battleCanCreatureCastThisSpell (sp, myNumber) == ESpellCastProblem::OK); else isCastingPossible = (curInt->cb->battleCanCastThisSpell (sp, myNumber) == ESpellCastProblem::OK); } 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 = -1; 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->attackerOwned && doubleWide ? 1 : 0); if(vstd::contains(occupyableHexes, destHex)) return destHex; else if(activeStack->attackerOwned) //if we are 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->attackerOwned) //if we are 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 8: //from left { if(activeStack->doubleWide() && !activeStack->attackerOwned) { std::vector acc = curInt->cb->battleGetAvailableHexes(activeStack, false); 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->attackerOwned) //if we are 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->attackerOwned && doubleWide ? 1 : 0); if(vstd::contains(occupyableHexes, destHex)) return destHex; else if(activeStack->attackerOwned) //if we are 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->attackerOwned) { std::vector acc = curInt->cb->battleGetAvailableHexes(activeStack, false); 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(attackingHeroInstance->tempOwner == curInt->cb->getMyColor()) //if we are 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(attackingHeroInstance->tempOwner == curInt->cb->getMyColor()) //if we are 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(); int effectID = -1; soundBase::soundID sound = soundBase::invalid;//FIXME: variable set but unused. Missing soundh->playSound()? std::string defname; switch(oi.obstacleType) { case CObstacleInstance::QUICKSAND: effectID = 55; sound = soundBase::QUIKSAND; break; case CObstacleInstance::LAND_MINE: effectID = 47; sound = soundBase::LANDMINE; break; case CObstacleInstance::FORCE_FIELD: { auto &spellObstacle = dynamic_cast(oi); if(spellObstacle.casterSide) { if(oi.getAffectedTiles().size() < 3) defname = "C15SPE0.DEF"; //TODO cannot find def for 2-hex force field \ appearing else defname = "C15SPE6.DEF"; } else { if(oi.getAffectedTiles().size() < 3) defname = "C15SPE0.DEF"; else defname = "C15SPE9.DEF"; } } sound = soundBase::FORCEFLD; break; case CObstacleInstance::FIRE_WALL: if(oi.getAffectedTiles().size() < 3) effectID = 43; //small fire wall appearing else effectID = 44; //and the big one sound = soundBase::fireWall; break; default: logGlobal->errorStream() << "I don't know how to animate appearing obstacle of type " << (int)oi.obstacleType; return; } if(graphics->battleACToDef[effectID].empty()) { logGlobal->errorStream() << "Cannot find def for effect type " << effectID; return; } if(defname.empty() && effectID >= 0) defname = graphics->battleACToDef[effectID].front(); assert(!defname.empty()); //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(getObstacleImage(oi), oi); addNewAnim(new CSpellEffectAnimation(this, defname, 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); } 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 = new BattleAction(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); } } else { delete ba; boost::unique_lock un(*LOCPLINT->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) { walls[g] = BitmapHandler::loadBitmap( getSiegeName(g) ); } } CBattleInterface::SiegeHelper::~SiegeHelper() { for(auto & elem : walls) { SDL_FreeSurface(elem); } } 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 : return 2; case EWallState::DESTROYED : if(what == 2 || what == 3 || what == 8) // towers don't have separate image here 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 0: //background return prefix + "BACK.BMP"; case 1: //background wall { switch(town->town->faction->index) { case 5: case 4: case 1: case 6: return prefix + "TPW1.BMP"; default: return prefix + "TPWL.BMP"; } } case 2: //keep return prefix + "MAN" + addit + ".BMP"; case 3: //bottom tower return prefix + "TW1" + addit + ".BMP"; case 4: //bottom wall return prefix + "WA1" + addit + ".BMP"; case 5: //below gate return prefix + "WA3" + addit + ".BMP"; case 6: //over gate return prefix + "WA4" + addit + ".BMP"; case 7: //upper wall return prefix + "WA6" + addit + ".BMP"; case 8: //upper tower return prefix + "TW2" + addit + ".BMP"; case 9: //gate return prefix + "DRW" + addit + ".BMP"; case 10: //gate arch return prefix + "ARCH.BMP"; case 11: //bottom static wall return prefix + "WA2.BMP"; case 12: //upper static wall return prefix + "WA5.BMP"; case 13: //moat return prefix + "MOAT.BMP"; case 14: //mlip return prefix + "MLIP.BMP"; case 15: //keep creature cover return prefix + "MANC.BMP"; case 16: //bottom turret creature cover return prefix + "TW1C.BMP"; case 17: //upper turret creature cover return prefix + "TW2C.BMP"; default: return ""; } } /// What: 1. background wall, 2. keep, 3. bottom tower, 4. bottom wall, 5. wall below gate, /// 6. wall over gate, 7. upper wall, 8. upper tower, 9. gate, 10. gate arch, 11. bottom static wall, 12. upper static wall, 13. moat, 14. mlip, /// 15. keep turret cover, 16. lower turret cover, 17. upper turret cover void CBattleInterface::SiegeHelper::printPartOfWall(SDL_Surface * to, int what) { Point pos = Point(-1, -1); auto & ci = owner->siegeH->town->town->clientInfo; if (what >= 1 && what <= 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 == 13 || what == 14)) return; // no moat in Tower. TODO: remove hardcode somehow? if(pos.x != -1) { 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) blitAt(getObstacleImage(*oi), pos.x + oi->getInfo().width, pos.y + oi->getInfo().height, to); if (siegeH && siegeH->town->hasBuilt(BuildingID::CITADEL)) siegeH->printPartOfWall(to, 14); // show moat background } void CBattleInterface::showHighlightedHexes(SDL_Surface * to) { 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()) { if(spellToCast) //when casting spell { //calculating spell school level const CSpell & spToCast = *CGI->spellh->objects[spellToCast->additionalInfo]; ui8 schoolLevel = 0; auto caster = activeStack->attackerOwned ? attackingHeroInstance : defendingHeroInstance; if (caster) schoolLevel = caster->getSpellSchoolLevel(&spToCast); // printing shaded hex(es) auto shaded = spToCast.rangeInHexes(currentlyHoveredHex, schoolLevel, curInt->cb->battleGetMySide()); for(BattleHex shadedHex : shaded) { if ((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH -1)) showHighlightedHex(to, shadedHex); } } else if (active)//always highlight pointed hex { if (currentlyHoveredHex.getX() != 0 && currentlyHoveredHex.getX() != GameConstants::BFIELD_WIDTH - 1) showHighlightedHex(to, currentlyHoveredHex); } } } } if(activeStack && settings["battle"]["stackRange"].Bool()) { std::set set = curInt->cb->battleGetAttackedHexes(activeStack, currentlyHoveredHex, attackingHex); for(BattleHex hex : set) 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 ); for (BattleHex hex : v) showHighlightedHex(to, hex); } } } void CBattleInterface::showHighlightedHex(SDL_Surface * to, BattleHex hex) { 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); } 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... } SDL_Surface * image = idToProjectile[it->creID]->ourImages[it->frameNum].bitmap; SDL_Rect dst; dst.h = image->h; dst.w = image->w; dst.x = it->x - dst.w / 2; dst.y = it->y - dst.h / 2; if(it->reverse) { SDL_Surface * rev = CSDL_Ext::verticalFlip(image); CSDL_Ext::blit8bppAlphaTo24bpp(rev, nullptr, to, &dst); SDL_FreeSurface(rev); } else { CSDL_Ext::blit8bppAlphaTo24bpp(image, nullptr, to, &dst); } // 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]->ourImages.size(); } 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) { auto isAmountBoxVisible = [&](const CStack * stack) -> bool { if (stack->hasBonusOfType(Bonus::SIEGE_WEAPON)) // siege weapons are always singular return false; if (curInt->curAction) { if (curInt->curAction->stackNumber == stack->ID) // stack is currently taking an action return false; if (curInt->curAction->actionType == Battle::WALK_AND_ATTACK && // stack is being targeted stack->position == curInt->curAction->additionalInfo) return false; if (curInt->curAction->destinationTile == stack->position) // stack is moving return false; } return true; }; auto getEffectsPositivness = [&](const CStack * stack) -> int { int pos = 0; for(auto & spellId : stack->activeSpells()) { pos += CGI->spellh->objects[ 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 BattleHex nextPos = stack->position + (stack->attackerOwned ? 1 : -1); const bool edge = stack->position % GameConstants::BFIELD_WIDTH == (stack->attackerOwned ? GameConstants::BFIELD_WIDTH - 2 : 1); const bool moveInside = !edge && !stackCountOutsideHexes[nextPos]; int xAdd = (stack->attackerOwned ? 220 : 202) + (stack->doubleWide() ? 44 : 0) * (stack->attackerOwned ? +1 : -1) + (moveInside ? amountNormal->w + 10 : 0) * (stack->attackerOwned ? -1 : +1); int yAdd = 260 + ((stack->attackerOwned || moveInside) ? 0 : -15); //blitting amount background box SDL_Surface *amountBG = amountNormal; if(!stack->getSpellBonuses()->empty()) amountBG = getAmountBoxBackground(getEffectsPositivness(stack)); 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->count), 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) { SDL_Surface *toBlit = getObstacleImage(*obstacle); Point p = getObstaclePosition(toBlit, *obstacle); blitAt(toBlit, p.x, p.y, to); } } void CBattleInterface::showBattleEffects(SDL_Surface *to, const std::vector &battleEffects) { for(auto & elem : battleEffects) { int currentFrame = floor(elem->currentFrame); currentFrame %= elem->anim->ourImages.size(); SDL_Surface * bitmapToBlit = elem->anim->ourImages[currentFrame].bitmap; SDL_Rect temp_rect = genRect(bitmapToBlit->h, bitmapToBlit->w, elem->x, elem->y); SDL_BlitSurface(bitmapToBlit, nullptr, to, &temp_rect); } } 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; } //showing queue if(!bresult) queue->showAll(to); else queue->blitBg(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->position; }; BattleObjectsByHex sorted; // Sort creatures for (auto & stack : curInt->cb->battleGetAllStacks()) { if(creAnims.find(stack->ID) == creAnims.end()) //e.g. for summoned but not yet handled stacks continue; if (stack->position < 0) // turret shooters are handled separately continue; if(!creAnims[stack->ID]->isDead()) { if (!creAnims[stack->ID]->isMoving()) sorted.hex[stack->position].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->position].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 for(auto & obstacle : curInt->cb->battleGetAllObstacles()) { if(obstacle->obstacleType != CObstacleInstance::ABSOLUTE_OBSTACLE && obstacle->obstacleType != CObstacleInstance::MOAT) { sorted.hex[obstacle->pos].obstacles.push_back(obstacle); } } // Sort wall parts if (siegeH) { sorted.beforeAll.walls.push_back(1); // 1. background wall sorted.hex[135].walls.push_back(2); // 2. keep sorted.afterAll.walls.push_back(3); // 3. bottom tower sorted.hex[182].walls.push_back(4); // 4. bottom wall sorted.hex[130].walls.push_back(5); // 5. wall below gate, sorted.hex[62].walls.push_back(6); // 6. wall over gate sorted.hex[12].walls.push_back(7); // 7. upper wall sorted.beforeAll.walls.push_back(8); // 8. upper tower //sorted.hex[94].walls.push_back(9); // 9. gate // Not implemented it seems sorted.hex[112].walls.push_back(10); // 10. gate arch sorted.hex[165].walls.push_back(11); // 11. bottom static wall sorted.hex[45].walls.push_back(12); // 12. upper static wall if (siegeH && siegeH->town->hasBuilt(BuildingID::CITADEL)) { sorted.beforeAll.walls.push_back(13); // 13. moat //sorted.beforeAll.walls.push_back(14); // 14. mlip (moat background terrain), blit as absolute obstacle sorted.hex[135].walls.push_back(15); // 15. keep turret cover } if (siegeH && siegeH->town->hasBuilt(BuildingID::CASTLE)) { sorted.afterAll.walls.push_back(16); // 16. lower turret cover sorted.beforeAll.walls.push_back(17); // 17. upper turret cover } } 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); } } SDL_Surface * CBattleInterface::getObstacleImage(const CObstacleInstance &oi) { int frameIndex = (animCount+1) * 25 / getAnimSpeed(); switch(oi.obstacleType) { case CObstacleInstance::USUAL: return vstd::circularAt(idToObstacle.find(oi.ID)->second->ourImages, frameIndex).bitmap; case CObstacleInstance::ABSOLUTE_OBSTACLE: return idToAbsoluteObstacle.find(oi.ID)->second; case CObstacleInstance::QUICKSAND: return vstd::circularAt(quicksand->ourImages, frameIndex).bitmap; case CObstacleInstance::LAND_MINE: return vstd::circularAt(landMine->ourImages, frameIndex).bitmap; case CObstacleInstance::FIRE_WALL: return vstd::circularAt(fireWall->ourImages, frameIndex).bitmap; case CObstacleInstance::FORCE_FIELD: { auto &forceField = dynamic_cast(oi); if(forceField.getAffectedTiles().size() > 2) return vstd::circularAt(bigForceField[forceField.casterSide]->ourImages, frameIndex).bitmap; else return vstd::circularAt(smallForceField[forceField.casterSide]->ourImages, frameIndex).bitmap; } case CObstacleInstance::MOAT://moat is blitted by SiegeHelper, this shouldn't be called default: assert(0); return nullptr; } } Point CBattleInterface::getObstaclePosition(SDL_Surface *image, const CObstacleInstance &obstacle) { int offset = image->h % 42; if(obstacle.obstacleType == CObstacleInstance::USUAL) { if(obstacle.getInfo().blockedTiles.front() < 0 || offset > 37) //second or part is for holy ground ID=62,65,63 offset -= 42; } else if(obstacle.obstacleType == CObstacleInstance::QUICKSAND) { offset -= 42; } Rect r = hexPosition(obstacle.pos); r.y += 42 - image->h + 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); //preparating 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/* || oi.obstacleType == CObstacleInstance::MOAT*/) blitAt(getObstacleImage(*oi), oi->getInfo().width, oi->getInfo().height, backgroundWithHexes); } if(settings["battle"]["cellBorders"].Bool()) CSDL_Ext::blit8bppAlphaTo24bpp(cellBorders, nullptr, backgroundWithHexes, nullptr); 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); } } } 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->position == stackPos) { turret = stack; break; } } if (turret) { std::vector stackList(1, turret); showStacks(to, stackList); siegeH->printPartOfWall(to, piece); } } } } vcmi-0.98/client/battle/CBattleInterface.h000066400000000000000000000411401250671757600204710ustar00rootroot00000000000000#pragma once //#include "../../lib/CCreatureSet.h" #include "../../lib/ConstTransitivePtr.h" //may be reundant #include "../../lib/GameConstants.h" #include "CBattleAnimations.h" /* * 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 * */ class CLabel; class CCreatureSet; class CGHeroInstance; class CDefHandler; class CStack; class CCallback; class CButton; class CToggleButton; class CToggleGroup; struct BattleResult; struct BattleSpellCast; struct CObstacleInstance; template struct CondSh; struct SetStackEffect; struct 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; struct BattleAction; class CBattleGameInterface; /// Small struct which contains information about the id of the attacked stack, the damage dealt,... struct StackAttackedInfo { const CStack * defender; //attacked stack unsigned int 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; int maxFrame; CDefHandler * anim; //animation to display 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 CIntObject { 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, FRIENDLY_CREATURE_SPELL, HOSTILE_CREATURE_SPELL, RISING_SPELL, ANY_CREATURE, 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 }; private: SDL_Surface * background, * menu, * amountNormal, * amountNegative, * amountPositive, * amountEffNeutral, * cellBorders, * backgroundWithHexes; CButton * bOptions, * bSurrender, * bFlee, * bAutofight, * bSpell, * bWait, * bDefence, * bConsoleUp, * bConsoleDown, *btactNext, *btactEnd; CBattleConsole * console; CBattleHero * attackingHero, * defendingHero; //fighting heroes CStackQueue *queue; const CCreatureSet *army1, *army2; //copy of initial armies (for result window) const CGHeroInstance * attackingHeroInstance, * defendingHeroInstance; std::map< int, CCreatureAnimation * > creAnims; //animations of creatures from fighting armies (order by BattleInfo's stacks' ID) std::map< int, CDefHandler * > idToProjectile; //projectiles of creatures (creatureID, defhandler) std::map< int, CDefHandler * > idToObstacle; //obstacles located on the battlefield std::map< int, SDL_Surface * > idToAbsoluteObstacle; //obstacles located on the battlefield //TODO these should be loaded only when needed (and freed then) but I believe it's rather work for resource manager, //so I didn't implement that (having ongoing RM development) CDefHandler *landMine; CDefHandler *quicksand; CDefHandler *fireWall; CDefHandler *smallForceField[2], *bigForceField[2]; // [side] std::map< int, bool > 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 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 cats 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 PossibleActions spellSelMode; BattleAction * 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); //called when stack gets its turn void endCastingSpell(); //ends casting spell (eg. when spell has been cast or canceled) void printConsoleAttacked(const CStack * defender, int dmg, int killed, const CStack * attacker, bool Multiple); std::list projectiles; //projectiles flying on battlefield void giveCommand(Battle::ActionType action, BattleHex tile, ui32 stackID, si32 additional=-1, si32 selectedStack = -1); 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); //c-tor ~SiegeHelper(); //d-tor //filename getters //what: 0 - background, 1 - background wall, 2 - keep, 3 - bottom tower, 4 - bottom wall, // 5 - below gate, 6 - over gate, 7 - upper wall, 8 - upper tower, 9 - gate, // 10 - gate arch, 11 - bottom static 12 - upper static, 13 - moat, 14 - moat background, // 15 - keep battlement, 16 - bottom battlement, 17 - upper battlement; // state uses EWallState enum std::string getSiegeName(ui16 what) const; std::string getSiegeName(ui16 what, int state) const; void printPartOfWall(SDL_Surface * to, int what);//what: 1 - background wall, 2 - keep, 3 - bottom tower, 4 - bottom wall, 5 - below gate, 6 - over gate, 7 - upper wall, 8 - uppert tower, 9 - gate, 10 - gate arch, 11 - bottom static wall, 12 - upper static wall, 15 - keep creature cover, 16 - bottom turret creature cover, 17 - upper turret creature cover friend class CBattleInterface; } * siegeH; shared_ptr attackerInt, defenderInt; //because LOCPLINT is not enough in hotSeat 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); 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(); SDL_Surface *getObstacleImage(const CObstacleInstance &oi); Point getObstaclePosition(SDL_Surface *image, const CObstacleInstance &obstacle); void redrawBackgroundWithHexes(const CStack * activeStack); /** End of battle screen blitting methods */ public: std::list > pendingAnims; //currently displayed animations void addNewAnim(CBattleAnimation * anim); //adds new anim to pendingAnims ui32 animIDhelper; //for giving IDs for animations static CondSh animsAreDisplayed; //for waiting with the end of battle for end of anims CBattleInterface(const CCreatureSet * army1, const CCreatureSet * army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, shared_ptr att, shared_ptr defen); //c-tor ~CBattleInterface(); //d-tor //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; CondSh *givenCommand; //data != nullptr if we have i.e. moved current unit bool myTurn; //if true, interface is active (commands can be ordered) CBattleResultWindow * resWindow; //window of end of battle 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(); void deactivate(); void keyPressed(const SDL_KeyboardEvent & key); void mouseMoved(const SDL_MouseMotionEvent &sEvent); void clickRight(tribool down, bool previousState); void show(SDL_Surface *to); void showAll(SDL_Surface *to); //call-ins void startAction(const BattleAction* action); void newStack(const CStack * stack); //new stack appeared on battlefield void stackRemoved(int 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); //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 displayEffect(ui32 effect, int destTile, bool areaEffect = true); //displays custom effect on the battlefield void displaySpellEffect(SpellID spellID, BattleHex destinationTile, bool areaEffect = true); //displays spell`s affected animation void displaySpellHit(SpellID spellID, BattleHex destinationTile, bool areaEffect = true); //displays spell`s affected animation 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(); PossibleActions selectionTypeByPositiveness(const CSpell & spell); 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); const CGHeroInstance * currentHero() const; InfoAboutHero enemyHero() const; friend class CPlayerInterface; friend class CButton; friend class CInGameConsole; friend class CBattleResultWindow; friend class CBattleHero; friend class CSpellEffectAnimation; 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 CClickableHex; }; vcmi-0.98/client/battle/CBattleInterfaceClasses.cpp000066400000000000000000000525501250671757600223510ustar00rootroot00000000000000#include "StdInc.h" #include "CBattleInterfaceClasses.h" #include "CBattleInterface.h" #include "../CBitmapHandler.h" #include "../CDefHandler.h" #include "../CGameInfo.h" #include "../CMessage.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../CVideoHandler.h" #include "../Graphics.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/BattleState.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" /* * 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 * */ 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) { 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) { //animation of flag SDL_Rect temp_rect; if(flip) { temp_rect = genRect( flag->ourImages[flagAnim].bitmap->h, flag->ourImages[flagAnim].bitmap->w, pos.x + 61, pos.y + 39); } else { temp_rect = genRect( flag->ourImages[flagAnim].bitmap->h, flag->ourImages[flagAnim].bitmap->w, pos.x + 72, pos.y + 39); } CSDL_Ext::blit8bppAlphaTo24bpp( flag->ourImages[flagAnim].bitmap, nullptr, screen, &temp_rect); //animation of hero SDL_Rect rect = pos; CSDL_Ext::blit8bppAlphaTo24bpp(dh->ourImages[currentFrame].bitmap, nullptr, to, &rect); if ( ++animCount == 4 ) { animCount = 0; if ( ++flagAnim >= flag->ourImages.size()) 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::clickLeft(tribool down, bool previousState) { if(myOwner->spellDestSelectMode) //we are casting a spell return; if(!down && myHero != nullptr && myOwner->myTurn && myOwner->getCurrentPlayerInterface()->cb->battleCanCastSpell()) //check conditions { for(int it=0; itbfield[it]->hovered && myOwner->bfield[it]->strictHovered) return; } CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); auto spellWindow = new CSpellWindow(genRect(595, 620, (screen->w - 620)/2, (screen->h - 595)/2), myHero, myOwner->getCurrentPlayerInterface()); GH.pushInt(spellWindow); } } void CBattleHero::switchToNextPhase() { if (phase != nextPhase) { phase = nextPhase; //find first and last frames of our animation for (firstFrame = 0; firstFrame < dh->ourImages.size() && dh->ourImages[firstFrame].groupNumber != phase; firstFrame++); for (lastFrame = firstFrame; lastFrame < dh->ourImages.size() && dh->ourImages[lastFrame].groupNumber == phase; lastFrame++); } currentFrame = firstFrame; } CBattleHero::CBattleHero(const std::string & defName, bool flipG, PlayerColor player, const CGHeroInstance * hero, const CBattleInterface * owner): flip(flipG), myHero(hero), myOwner(owner), phase(1), nextPhase(0), flagAnim(0), animCount(0) { dh = CDefHandler::giveDef( defName ); for(auto & elem : dh->ourImages) //transforming images { if(flip) { SDL_Surface * hlp = CSDL_Ext::verticalFlip(elem.bitmap); SDL_FreeSurface(elem.bitmap); elem.bitmap = hlp; } CSDL_Ext::alphaTransform(elem.bitmap); } if(flip) flag = CDefHandler::giveDef("CMFLAGR.DEF"); else flag = CDefHandler::giveDef("CMFLAGL.DEF"); //coloring flag and adding transparency for(auto & elem : flag->ourImages) { CSDL_Ext::alphaTransform(elem.bitmap); graphics->blueToPlayersAdv(elem.bitmap, player); } addUsedEvents(LCLICK); switchToNextPhase(); } CBattleHero::~CBattleHero() { delete dh; delete flag; } CBattleOptionsWindow::CBattleOptionsWindow(const SDL_Rect & position, CBattleInterface *owner) { OBJ_CONSTRUCTION_CAPTURING_ALL; pos = position; background = new CPicture("comopbck.bmp"); background->colorize(owner->getCurrentPlayerInterface()->playerID); viewGrid = new CToggleButton(Point(25, 56), "sysopchk.def", CGI->generaltexth->zelp[427], [=](bool on){owner->setPrintCellBorders(on);} ); viewGrid->setSelected(settings["battle"]["cellBorders"].Bool()); movementShadow = new CToggleButton(Point(25, 89), "sysopchk.def", CGI->generaltexth->zelp[428], [=](bool on){owner->setPrintStackRange(on);}); movementShadow->setSelected(settings["battle"]["stackRange"].Bool()); mouseShadow = new CToggleButton(Point(25, 122), "sysopchk.def", CGI->generaltexth->zelp[429], [=](bool on){owner->setPrintMouseShadow(on);}); mouseShadow->setSelected(settings["battle"]["mouseShadow"].Bool()); animSpeeds = new CToggleGroup([=](int value){ owner->setAnimSpeed(value);}); animSpeeds->addToggle(40, new CToggleButton(Point( 28, 225), "sysopb9.def", CGI->generaltexth->zelp[422])); animSpeeds->addToggle(63, new CToggleButton(Point( 92, 225), "sysob10.def", CGI->generaltexth->zelp[423])); animSpeeds->addToggle(100, new CToggleButton(Point(156, 225), "sysob11.def", CGI->generaltexth->zelp[424])); animSpeeds->setSelected(owner->getAnimSpeed()); setToDefault = new CButton (Point(246, 359), "codefaul.def", CGI->generaltexth->zelp[393], [&]{ bDefaultf(); }); setToDefault->setImageOrder(1, 0, 2, 3); exit = new CButton (Point(357, 359), "soretrn.def", CGI->generaltexth->zelp[392], [&]{ bExitf();}, SDLK_RETURN); exit->setImageOrder(1, 0, 2, 3); //creating labels labels.push_back(new CLabel(242, 32, FONT_BIG, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[392]));//window title labels.push_back(new CLabel(122, 214, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[393]));//animation speed labels.push_back(new CLabel(122, 293, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[394]));//music volume labels.push_back(new CLabel(122, 359, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[395]));//effects' volume labels.push_back(new CLabel(353, 66, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[396]));//auto - combat options labels.push_back(new CLabel(353, 265, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[397]));//creature info //auto - combat options labels.push_back(new CLabel(283, 86, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[398]));//creatures labels.push_back(new CLabel(283, 116, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[399]));//spells labels.push_back(new CLabel(283, 146, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[400]));//catapult labels.push_back(new CLabel(283, 176, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[151]));//ballista labels.push_back(new CLabel(283, 206, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[401]));//first aid tent //creature info labels.push_back(new CLabel(283, 285, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[402]));//all stats labels.push_back(new CLabel(283, 315, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[403]));//spells only //general options labels.push_back(new CLabel(61, 57, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[404])); labels.push_back(new CLabel(61, 90, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[405])); labels.push_back(new CLabel(61, 123, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[406])); labels.push_back(new CLabel(61, 156, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[407])); } void CBattleOptionsWindow::bDefaultf() { //TODO: implement } void CBattleOptionsWindow::bExitf() { GH.popIntTotally(this); } CBattleResultWindow::CBattleResultWindow(const BattleResult &br, const SDL_Rect & pos, CPlayerInterface &_owner) : owner(_owner) { OBJ_CONSTRUCTION_CAPTURING_ALL; this->pos = pos; CPicture * bg = new CPicture("CPRESULT"); bg->colorize(owner.playerID); exit = new CButton (Point(384, 505), "iok6432.def", std::make_pair("", ""), [&]{ bExitf();}, SDLK_RETURN); exit->borderColor = Colors::METALLIC_GOLD; if(br.winner==0) //attacker won { new CLabel( 59, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]); new CLabel(408, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]); } else //if(br.winner==1) { new CLabel( 59, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]); new CLabel(412, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]); } new CLabel(232, 302, FONT_BIG, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[407]); new CLabel(232, 332, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408]); new CLabel(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 { new CAnimImage("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->attackerOwned == 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... { new CAnimImage("TWCRPORT", (*best)->type->idNumber+2, 0, xs[i], 38); sideNames[i] = CGI->creh->creatures[(*best)->type->idNumber]->namePl; } } } //printing attacker and defender's names new CLabel( 89, 37, FONT_SMALL, TOPLEFT, Colors::WHITE, sideNames[0]); new CLabel( 381, 53, FONT_SMALL, BOTTOMRIGHT, Colors::WHITE, sideNames[1]); //printing casualties for(int step = 0; step < 2; ++step) { if(br.casualties[step].size()==0) { new CLabel( 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]) { new CAnimImage("CPRSMALL", CGI->creh->creatures[elem.first]->iconIndex, 0, xPos, yPos); std::ostringstream amount; amount<battleGetMySide()); if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won { int text=-1; switch(br.result) { case BattleResult::NORMAL: text = 304; break; case BattleResult::ESCAPE: text = 303; break; case BattleResult::SURRENDER: text = 302; 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])); } new CTextBox(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, CENTER, Colors::WHITE); } else // we lose { switch(br.result) { case BattleResult::NORMAL: { CCS->musich->playMusic("Music/LoseCombat", false); CCS->videoh->open("LBSTART.BIK"); new CLabel(235, 235, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[311]); break; } case BattleResult::ESCAPE: //flee { CCS->musich->playMusic("Music/Retreat Battle", false); CCS->videoh->open("RTSTART.BIK"); new CLabel(235, 235, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[310]); break; } case BattleResult::SURRENDER: { CCS->musich->playMusic("Music/Surrender Battle", false); CCS->videoh->open("SURRENDER.BIK"); new CLabel(235, 235, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[309]); break; } } } } CBattleResultWindow::~CBattleResultWindow() { } 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() { if(LOCPLINT->cb->getStartInfo()->mode == StartInfo::DUEL) { CGuiHandler::pushSDLEvent(SDL_QUIT); return; } CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon GH.popIntTotally(this); if(dynamic_cast(GH.topInt())) 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->position < 0) //creatures in turrets { switch(stack->position) { 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->attackerOwned) { 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), hovered(false), 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()) { const std::string & attackedName = attackedStack->count == 1 ? attackedStack->getCreature()->nameSing : attackedStack->getCreature()->namePl; auto txt = boost::format (CGI->generaltexth->allTexts[220]) % attackedName; myInterface->console->alterTxt = boost::to_string(txt); 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.pushInt(new CStackWindow(myst, true)); } } } void CStackQueue::update() { stacksSorted.clear(); owner->getCurrentPlayerInterface()->cb->battleGetStackQueue(stacksSorted, stackBoxes.size()); if(stacksSorted.size()) { for (int i = 0; i < stackBoxes.size() ; i++) { stackBoxes[i]->setStack(stacksSorted[i]); } } else { //no stacks on battlefield... what to do with queue? } } CStackQueue::CStackQueue(bool Embedded, CBattleInterface * _owner) :embedded(Embedded), owner(_owner) { OBJ_CONSTRUCTION_CAPTURING_ALL; if(embedded) { bg = nullptr; pos.w = QUEUE_SIZE * 37; pos.h = 46; pos.x = screen->w/2 - pos.w/2; pos.y = (screen->h - 600)/2 + 10; } else { bg = BitmapHandler::loadBitmap("DIBOXBCK"); pos.w = 800; pos.h = 85; } stackBoxes.resize(QUEUE_SIZE); for (int i = 0; i < stackBoxes.size(); i++) { stackBoxes[i] = new StackBox(embedded); stackBoxes[i]->moveBy(Point(1 + (embedded ? 36 : 80)*i, 0)); } } CStackQueue::~CStackQueue() { SDL_FreeSurface(bg); } void CStackQueue::showAll(SDL_Surface * to) { blitBg(to); CIntObject::showAll(to); } void CStackQueue::blitBg( SDL_Surface * to ) { if(bg) { SDL_SetClipRect(to, &pos); CSDL_Ext::fillTexture(to, bg); SDL_SetClipRect(to, nullptr); } } void CStackQueue::StackBox::showAll(SDL_Surface * to) { assert(stack); bg->colorize(stack->owner); CIntObject::showAll(to); if(small) printAtMiddleLoc(makeNumberShort(stack->count), pos.w/2, pos.h - 7, FONT_SMALL, Colors::WHITE, to); else printAtMiddleLoc(makeNumberShort(stack->count), pos.w/2, pos.h - 8, FONT_MEDIUM, Colors::WHITE, to); } void CStackQueue::StackBox::setStack( const CStack *stack ) { this->stack = stack; assert(stack); icon->setFrame(stack->getCreature()->iconIndex); } CStackQueue::StackBox::StackBox(bool small): stack(nullptr), small(small) { OBJ_CONSTRUCTION_CAPTURING_ALL; bg = new CPicture(small ? "StackQueueSmall" : "StackQueueLarge" ); if (small) { icon = new CAnimImage("CPRSMALL", 0, 0, 5, 2); } else icon = new CAnimImage("TWCRPORT", 0, 0, 9, 1); pos.w = bg->pos.w; pos.h = bg->pos.h; } vcmi-0.98/client/battle/CBattleInterfaceClasses.h000066400000000000000000000121171250671757600220110ustar00rootroot00000000000000#pragma once #include "../gui/CIntObject.h" #include "../../lib/BattleHex.h" struct SDL_Surface; class CDefHandler; class CGHeroInstance; class CBattleInterface; class CPicture; class CButton; class CToggleButton; class CToggleGroup; class CLabel; struct BattleResult; class CStack; class CAnimImage; class CPlayerInterface; /* * 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 * */ /// 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); 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 CDefHandler *dh, *flag; //animation and flag 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 ui8 flagAnim, animCount; //for flag animation void show(SDL_Surface * to); //prints next frame of animation to to void setPhase(int newPhase); //sets phase of hero animation void clickLeft(tribool down, bool previousState); //call-in CBattleHero(const std::string &defName, bool filpG, PlayerColor player, const CGHeroInstance *hero, const CBattleInterface *owner); //c-tor ~CBattleHero(); //d-tor }; /// Class which manages the battle options window class CBattleOptionsWindow : public CIntObject { private: CPicture * background; CButton * setToDefault, * exit; CToggleButton * viewGrid, * movementShadow, * mouseShadow; CToggleGroup * animSpeeds; std::vector labels; public: CBattleOptionsWindow(const SDL_Rect &position, CBattleInterface *owner); //c-tor void bDefaultf(); //default button callback void bExitf(); //exit button callback }; /// Class which is responsible for showing the battle result window class CBattleResultWindow : public CIntObject { private: CButton *exit; CPlayerInterface &owner; public: CBattleResultWindow(const BattleResult & br, const SDL_Rect & pos, CPlayerInterface &_owner); //c-tor ~CBattleResultWindow(); //d-tor void bExitf(); //exit button callback void activate(); void show(SDL_Surface * to = 0); }; /// 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 hovered, 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); void mouseMoved (const SDL_MouseMotionEvent &sEvent); void clickLeft(tribool down, bool previousState); void clickRight(tribool down, bool previousState); CClickableHex(); }; /// Shows the stack queue class CStackQueue : public CIntObject { class StackBox : public CIntObject { public: CPicture * bg; CAnimImage * icon; const CStack *stack; bool small; void showAll(SDL_Surface * to); void setStack(const CStack *nStack); StackBox(bool small); }; public: static const int QUEUE_SIZE = 10; const bool embedded; std::vector stacksSorted; std::vector stackBoxes; SDL_Surface * bg; CBattleInterface * owner; CStackQueue(bool Embedded, CBattleInterface * _owner); ~CStackQueue(); void update(); void showAll(SDL_Surface *to); void blitBg(SDL_Surface * to); }; vcmi-0.98/client/battle/CCreatureAnimation.cpp000066400000000000000000000317151250671757600214110ustar00rootroot00000000000000#include "StdInc.h" #include "CCreatureAnimation.h" #include "../../lib/vcmi_endian.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CBinaryReader.h" #include "../../lib/filesystem/CMemoryStream.h" #include "../gui/SDL_Extensions.h" #include "../gui/SDL_Pixels.h" /* * 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 * */ 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; } CCreatureAnimation * AnimationControls::getAnimation(const CCreature * creature) { auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2); return new CCreatureAnimation(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: 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: 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: return speed; default: assert(0); return 1; } } 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) { assert(type >= 0); assert(framesInGroup(type) != 0); this->type = type; currentFrame = 0; once = false; play(); } CCreatureAnimation::CCreatureAnimation(std::string name, TSpeedController controller) : defName(name), speed(0.1), currentFrame(0), elapsedTime(0), type(CCreatureAnim::HOLDING), border(CSDL_Ext::makeColor(0, 0, 0, 0)), speedController(controller), once(false) { // separate block to avoid accidental use of "data" after it was moved into "pixelData" { ResourceID resID(std::string("SPRITES/") + name, EResType::ANIMATION); auto data = CResourceHandler::get()->load(resID)->readAll(); pixelData = std::move(data.first); pixelDataSize = data.second; } CBinaryReader reader(new CMemoryStream(pixelData.get(), pixelDataSize)); reader.readInt32(); // def type, unused fullWidth = reader.readInt32(); fullHeight = reader.readInt32(); int totalBlocks = reader.readInt32(); for (auto & elem : palette) { elem.r = reader.readUInt8(); elem.g = reader.readUInt8(); elem.b = reader.readUInt8(); CSDL_Ext::colorSetAlpha(elem,0); } for (int i=0; i= 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) { #ifdef VCMI_SDL1 return CSDL_Ext::makeColor(base.r, base.g, base.b, ui8(base.unused * alpha / 256)); #else return CSDL_Ext::makeColor(base.r, base.g, base.b, ui8(base.a * alpha / 256)); #endif } 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) { #ifdef VCMI_SDL1 return CSDL_Ext::makeColor( mixChannels(over.r, base.r, over.unused, base.unused), mixChannels(over.g, base.g, over.unused, base.unused), mixChannels(over.b, base.b, over.unused, base.unused), ui8(over.unused + base.unused * (255 - over.unused) / 256) ); #else 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) ); #endif // VCMI_SDL1 } std::array CCreatureAnimation::genSpecialPalette() { std::array ret; ret[0] = genShadow(0); ret[1] = genShadow(64); ret[2] = genShadow(128); ret[3] = genShadow(128); ret[4] = genShadow(128); ret[5] = genBorderColor(getBorderStrength(elapsedTime), border); ret[6] = addColors(genShadow(128), genBorderColor(getBorderStrength(elapsedTime), border)); ret[7] = addColors(genShadow(64), genBorderColor(getBorderStrength(elapsedTime), border)); return ret; } template void CCreatureAnimation::nextFrameT(SDL_Surface * dest, bool rotate) { assert(dataOffsets.count(type) && dataOffsets.at(type).size() > size_t(currentFrame)); ui32 offset = dataOffsets.at(type).at(floor(currentFrame)); CBinaryReader reader(new CMemoryStream(pixelData.get(), pixelDataSize)); reader.getStream()->seek(offset); reader.readUInt32(); // unused, size of pixel data for this frame const ui32 defType2 = reader.readUInt32(); const ui32 fullWidth = reader.readUInt32(); /*const ui32 fullHeight =*/ reader.readUInt32(); const ui32 spriteWidth = reader.readUInt32(); const ui32 spriteHeight = reader.readUInt32(); const int leftMargin = reader.readInt32(); const int topMargin = reader.readInt32(); const int rightMargin = fullWidth - spriteWidth - leftMargin; //const int bottomMargin = fullHeight - spriteHeight - topMargin; const size_t baseOffset = reader.getStream()->tell(); assert(defType2 == 1); UNUSED(defType2); auto specialPalette = genSpecialPalette(); for (ui32 i=0; i(dest, destX + (rotate?(-j):(j)), destY, lineData[currentOffset + j], specialPalette); currentOffset += length; } else// RLE { if (type != 0) // transparency row, handle it here for speed { for (size_t j=0; j(dest, destX + (rotate?(-j):(j)), destY, type, specialPalette); } } destX += rotate ? (-length) : (length); totalRowLength += length; } } } void CCreatureAnimation::nextFrame(SDL_Surface *dest, bool attacker) { // Note: please notice that attacker value is inversed when passed further. // This is intended behavior because "attacker" actually does not needs rotation switch(dest->format->BytesPerPixel) { case 2: return nextFrameT<2>(dest, !attacker); case 3: return nextFrameT<3>(dest, !attacker); case 4: return nextFrameT<4>(dest, !attacker); default: logGlobal->errorStream() << (int)dest->format->BitsPerPixel << " bpp is not supported!!!"; } } int CCreatureAnimation::framesInGroup(CCreatureAnim::EAnimType group) const { if(dataOffsets.count(group) == 0) return 0; return dataOffsets.at(group).size(); } ui8 * CCreatureAnimation::getPixelAddr(SDL_Surface * dest, int X, int Y) const { return (ui8*)dest->pixels + X * dest->format->BytesPerPixel + Y * dest->pitch; } template inline void CCreatureAnimation::putPixelAt(SDL_Surface * dest, int X, int Y, size_t index, const std::array & special) const { if ( X < pos.x + pos.w && Y < pos.y + pos.h && X >= 0 && Y >= 0) putPixel(getPixelAddr(dest, X, Y), palette[index], index, special); } template inline void CCreatureAnimation::putPixel(ui8 * dest, const SDL_Color & color, size_t index, const std::array & special) const { if (index < 8) { const SDL_Color & pal = special[index]; #ifdef VCMI_SDL1 ColorPutter::PutColor(dest, pal.r, pal.g, pal.b, pal.unused); #else ColorPutter::PutColor(dest, pal.r, pal.g, pal.b, pal.a); #endif // 0 } else { ColorPutter::PutColor(dest, color.r, color.g, color.b); } } bool CCreatureAnimation::isDead() const { return getType() == CCreatureAnim::DEAD || getType() == CCreatureAnim::DEATH; } 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() { speed = 0; if (speedController(this, type) != 0) speed = 1 / speedController(this, type); } vcmi-0.98/client/battle/CCreatureAnimation.h000066400000000000000000000113461250671757600210540ustar00rootroot00000000000000#pragma once #include "../../lib/FunctionList.h" #include "../gui/SDL_Extensions.h" #include "../widgets/Images.h" /* * 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 * */ 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 CCreatureAnimation * 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 defName; int fullWidth, fullHeight; // palette, as read from def file std::array palette; //key = id of group (note that some groups may be missing) //value = offset of pixel data for each frame, vector size = number of frames in group std::map> dataOffsets; //animation raw data //TODO: use vector instead? unique_ptr pixelData; size_t pixelDataSize; // 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 ui8 * getPixelAddr(SDL_Surface * dest, int ftcpX, int ftcpY) const; template void putPixelAt(SDL_Surface * dest, int X, int Y, size_t index, const std::array & special) const; template void putPixel( ui8 * dest, const SDL_Color & color, size_t index, const std::array & special) const; template void nextFrameT(SDL_Surface * dest, bool rotate); void endAnimation(); /// creates 8 special colors for current frame std::array genSpecialPalette(); 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(std::string name, TSpeedController speedController); //c-tor 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 rotate); // 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; //retirns number of fromes in given group void pause(); void play(); //helpers. TODO: move them somewhere else bool isDead() const; bool isIdle() const; bool isMoving() const; bool isShooting() const; }; vcmi-0.98/client/gui/000077500000000000000000000000001250671757600144725ustar00rootroot00000000000000vcmi-0.98/client/gui/CAnimation.cpp000066400000000000000000000746041250671757600172330ustar00rootroot00000000000000#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" /* * 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 * */ typedef std::map > source_map; typedef std::map image_map; typedef std::map group_map; 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(); }; class CompImageLoader { CompImage * image; ui8 *position; ui8 *entry; ui32 currentLine; inline ui8 typeOf(ui8 color); inline void NewEntry(ui8 color, size_t size); inline void NewEntry(const ui8 * &data, size_t size); 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); CompImageLoader(CompImage * Img); ~CompImageLoader(); }; // 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; ui8 * data; ui8 * getCopy() { auto ret = new ui8[size]; std::copy(data, data + size, ret); return ret; } FileData(): size(0), data(nullptr) {} ~FileData() { delete [] data; } }; std::list cache; public: ui8 * 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(); cache.push_back(FileData()); auto data = CResourceHandler::get()->load(rid)->readAll(); cache.back().name = ResourceID(rid); cache.back().size = data.second; cache.back().data = data.first.release(); return cache.back().getCopy(); } }; static CFileCache animationCache; /************************************************************************* * DefFile, class used for def loading * *************************************************************************/ CDefFile::CDefFile(std::string Name): data(nullptr), palette(nullptr) { //First 8 colors in def palette used for transparency static SDL_Color H3Palette[8] = { { 0, 0, 0, 0},// 100% - transparency { 0, 0, 0, 64},// 75% - shadow border, { 0, 0, 0, 128},// 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 = new SDL_Color[256]; int it = 0; ui32 type = read_le_u32(data + 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 + 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++]; CSDL_Ext::colorSetAlpha(palette[i],255); } if (type == 71 || type == 64)//Buttons/buildings don't have shadows\semi-transparency memset(palette, 0, sizeof(SDL_Color)*2); else memcpy(palette, H3Palette, sizeof(SDL_Color)*8);//initialize shadow\selection colors 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+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); ui32 BaseOffset = sizeof(SSpriteDef); loader.init(Point(sprite.width, sprite.height), Point(sprite.leftMargin, sprite.topMargin), Point(sprite.fullWidth, sprite.fullHeight), palette); 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; ierrorStream()<<"Error: unsupported format of def file: "< 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(SDL_SWSURFACE, SpriteSize.x, SpriteSize.y, 8, 0, 0, 0, 0); image->margins = Margins; image->fullSize = FullSize; //Prepare surface SDL_SetColors(image->surf, pal, 0, 256); 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_SRCCOLORKEY, 0); //TODO: RLE if compressed and bpp>1 } //////////////////////////////////////////////////////////////////////////////// CompImageLoader::CompImageLoader(CompImage * Img): image(Img), position(nullptr), entry(nullptr), currentLine(0) { } void CompImageLoader::init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) { image->sprite = Rect(Margins, SpriteSize); image->fullSize = FullSize; if (SpriteSize.x && SpriteSize.y) { image->palette = new SDL_Color[256]; memcpy((void*)image->palette, (void*)pal, 256*sizeof(SDL_Color)); //Allocate enought space for worst possible case, c-style malloc used due to resizing after load image->surf = (ui8*)malloc(SpriteSize.x*SpriteSize.y*3); image->line = new ui32[SpriteSize.y+1]; image->line[0] = 0; position = image->surf; } } inline void CompImageLoader::NewEntry(ui8 color, size_t size) { assert(color != 0xff); assert(size && size<256); entry = position; entry[0] = color; entry[1] = size; position +=2; } inline void CompImageLoader::NewEntry(const ui8 * &data, size_t size) { assert(size && size<256); entry = position; entry[0] = 0xff; entry[1] = size; position +=2; memcpy(position, data, size); position+=size; data+=size; } inline ui8 CompImageLoader::typeOf(ui8 color) { if (color == 0) return 0; #ifdef VCMI_SDL1 if (image->palette[color].unused != 255) return 1; #else if (image->palette[color].a != 255) return 1; #endif // 0 return 2; } inline void CompImageLoader::Load(size_t size, const ui8 * data) { while (size) { //Try to compress data while(true) { ui8 color = data[0]; if (color != 0xff) { size_t runLength = 1; while (runLength < size && color == data[runLength]) runLength++; if (runLength > 1 && runLength < 255)//Row of one color found - use RLE { Load(runLength, color); data += runLength; size -= runLength; if (!size) return; } else break; } else break; } //Select length for new raw entry size_t runLength = 1; ui8 color = data[0]; ui8 type = typeOf(color); ui8 color2; ui8 type2; if (size > 1) { do { color2 = data[runLength]; type2 = typeOf(color2); runLength++; } //While we have data of this type and different colors while ((runLength < size) && (type == type2) && ( (color2 != 0xff) || (color2 != color))); } size -= runLength; //add data to last entry if (entry && entry[0] == 0xff && type == typeOf(entry[2])) { size_t toCopy = std::min(runLength, 255 - entry[1]); runLength -= toCopy; entry[1] += toCopy; memcpy(position, data, toCopy); data+=toCopy; position+=toCopy; } //Create new entries while (runLength > 255) { NewEntry(data, 255); runLength -= 255; } if (runLength) NewEntry(data, runLength); } } inline void CompImageLoader::Load(size_t size, ui8 color) { if (!size) return; if (color==0xff) { auto tmpbuf = new ui8[size]; memset((void*)tmpbuf, color, size); Load(size, tmpbuf); delete [] tmpbuf; return; } //Current entry is RLE with same color as new block if (entry && entry[0] == color) { size_t toCopy = std::min(size, 255 - entry[1]); size -= toCopy; entry[1] += toCopy; } //Create new entries while (size > 255) { NewEntry(color, 255); size -= 255; } if (size) NewEntry(color, size); } void CompImageLoader::EndLine() { currentLine++; image->line[currentLine] = position - image->surf; entry = nullptr; } CompImageLoader::~CompImageLoader() { if (!image->surf) return; ui8* newPtr = (ui8*)realloc((void*)image->surf, position - image->surf); if (newPtr) image->surf = newPtr; } /************************************************************************* * Classes for images, support loading from file and drawing on surface * *************************************************************************/ IImage::IImage(): refCount(1) { } bool IImage::decreaseRef() { refCount--; return refCount <= 0; } void IImage::increaseRef() { refCount++; } SDLImage::SDLImage(CDefFile *data, size_t frame, size_t group, bool compressed): surf(nullptr) { SDLImageLoader loader(this); data->loadFrame(frame, group, loader); } SDLImage::SDLImage(SDL_Surface * from, bool extraRef): margins(0,0) { surf = from; if (extraRef) surf->refcount++; fullSize.x = surf->w; fullSize.y = surf->h; } SDLImage::SDLImage(std::string filename, bool compressed): margins(0,0) { surf = BitmapHandler::loadBitmap(filename); if (surf == nullptr) { logGlobal->errorStream() << "Error: failed to load image "<w; fullSize.y = surf->h; } if (compressed) { SDL_Surface *temp = surf; // add RLE flag #ifdef VCMI_SDL1 if (surf->format->palette) { const SDL_Color &c = temp->format->palette->colors[0]; SDL_SetColorKey(temp, (SDL_SRCCOLORKEY | SDL_RLEACCEL), SDL_MapRGB(temp -> format, c.r, c.g, c.b)); } else SDL_SetColorKey(temp, SDL_RLEACCEL, 0); #else if (surf->format->palette) { CSDL_Ext::setColorKey(temp,temp->format->palette->colors[0]); } SDL_SetSurfaceRLE(temp, SDL_RLEACCEL); #endif // convert surface to enable RLE surf = SDL_ConvertSurface(temp, temp->format, temp->flags); SDL_FreeSurface(temp); } } void SDLImage::draw(SDL_Surface *where, int posX, int posY, Rect *src, ui8 rotation) const { if (!surf) return; Rect sourceRect(margins.x, margins.y, surf->w, surf->h); //TODO: rotation and scaling if (src) { sourceRect = sourceRect & *src; } Rect destRect(posX, posY, surf->w, surf->h); destRect += sourceRect.topLeft(); sourceRect -= margins; CSDL_Ext::blitSurface(surf, &sourceRect, where, &destRect); } void SDLImage::playerColored(PlayerColor player) { graphics->blueToPlayersAdv(surf, player); } int SDLImage::width() const { return fullSize.x; } int SDLImage::height() const { return fullSize.y; } SDLImage::~SDLImage() { SDL_FreeSurface(surf); } CompImage::CompImage(const CDefFile *data, size_t frame, size_t group): surf(nullptr), line(nullptr), palette(nullptr) { CompImageLoader loader(this); data->loadFrame(frame, group, loader); } CompImage::CompImage(SDL_Surface * surf) { //TODO assert(0); } void CompImage::draw(SDL_Surface *where, int posX, int posY, Rect *src, ui8 alpha) const { int rotation = 0; //TODO //rotation & 2 = horizontal rotation //rotation & 4 = vertical rotation if (!surf) return; Rect sourceRect(sprite); //TODO: rotation and scaling if (src) sourceRect = sourceRect & *src; //Limit source rect to sizes of surface sourceRect = sourceRect & Rect(0, 0, where->w, where->h); //Starting point on SDL surface Point dest(posX+sourceRect.x, posY+sourceRect.y); if (rotation & 2) dest.y += sourceRect.h; if (rotation & 4) dest.x += sourceRect.w; sourceRect -= sprite.topLeft(); for (int currY = 0; currY size ) { currX -= size; if (type == 0xff) data += size; type = *(data++); size = *(data++); } //This block will be shown partially - calculate size\position size -= currX; if (type == 0xff) data += currX; currX = 0; ui8 bpp = where->format->BytesPerPixel; //Calculate position for blitting: pixels + Y + X ui8* blitPos = (ui8*) where->pixels; if (rotation & 4) blitPos += (dest.y - currY) * where->pitch; else blitPos += (dest.y + currY) * where->pitch; blitPos += dest.x * bpp; //Blit blocks that must be fully visible while (currX + size < sourceRect.w) { //blit block, pointers will be modified if needed BlitBlockWithBpp(bpp, type, size, data, blitPos, alpha, rotation & 2); currX += size; type = *(data++); size = *(data++); } //Blit last, semi-visible block size = sourceRect.w - currX; BlitBlockWithBpp(bpp, type, size, data, blitPos, alpha, rotation & 2); } } #define CASEBPP(x,y) case x: BlitBlock(type, size, data, dest, alpha); break //FIXME: better way to get blitter void CompImage::BlitBlockWithBpp(ui8 bpp, ui8 type, ui8 size, ui8 *&data, ui8 *&dest, ui8 alpha, bool rotated) const { assert(bpp>1 && bpp<5); if (rotated) switch (bpp) { CASEBPP(2,1); CASEBPP(3,1); CASEBPP(4,1); } else switch (bpp) { CASEBPP(2,1); CASEBPP(3,1); CASEBPP(4,1); } } #undef CASEBPP //Blit one block from RLE-d surface template void CompImage::BlitBlock(ui8 type, ui8 size, ui8 *&data, ui8 *&dest, ui8 alpha) const { //Raw data if (type == 0xff) { ui8 color = *data; if (alpha != 255)//Per-surface alpha is set { for (size_t i=0; i::PutColorAlpha(dest, col); } return; } #ifdef VCMI_SDL1 if (palette[color].unused == 255) #else if (palette[color].a == 255) #endif // 0 { //Put row of RGB data for (size_t i=0; i::PutColor(dest, palette[*(data++)]); } else { //Put row of RGBA data for (size_t i=0; i::PutColorAlpha(dest, palette[*(data++)]); } } //RLE-d sequence else { #ifdef VCMI_SDL1 if (alpha != 255 && palette[type].unused !=0)//Per-surface alpha is set { SDL_Color col = palette[type]; col.unused = (int)col.unused*(255-alpha)/255; for (size_t i=0; i::PutColorAlpha(dest, col); return; } switch (palette[type].unused) #else if (alpha != 255 && palette[type].a !=0)//Per-surface alpha is set { SDL_Color col = palette[type]; col.a = (int)col.a*(255-alpha)/255; for (size_t i=0; i::PutColorAlpha(dest, col); return; } switch (palette[type].a) #endif // 0 { case 0: { //Skip row dest += size*bpp; break; } case 255: { //Put RGB row ColorPutter::PutColorRow(dest, palette[type], size); break; } default: { //Put RGBA row for (size_t i=0; i::PutColorAlpha(dest, palette[type]); break; } } } } void CompImage::playerColored(PlayerColor player) { SDL_Color *pal = nullptr; if(player < PlayerColor::PLAYER_LIMIT) { pal = graphics->playerColorPalette + 32*player.getNum(); } else if(player == PlayerColor::NEUTRAL) { pal = graphics->neutralColorPalette; } else assert(0); for(int i=0; i<32; ++i) { CSDL_Ext::colorAssign(palette[224+i],pal[i]); } } int CompImage::width() const { return fullSize.x; } int CompImage::height() const { return fullSize.y; } CompImage::~CompImage() { free(surf); delete [] line; delete [] palette; } /************************************************************************* * CAnimation for animations handling, can load part of file if needed * *************************************************************************/ IImage * 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) { group = frame; frame = atoi(filename.c_str()+pos); } anim.load(frame ,group); IImage * ret = anim.images[group][frame]; anim.images.clear(); return ret; } bool CAnimation::loadFrame(CDefFile * file, size_t frame, size_t group) { if (size(group) <= frame) { printError(frame, group, "LoadFrame"); return false; } IImage *image = getImage(frame, group, false); if (image) { image->increaseRef(); return true; } //try to get image from def if (source[group][frame].getType() == JsonNode::DATA_NULL) { if (file) { auto frameList = file->getEntries(); if (vstd::contains(frameList, group) && frameList.at(group) > frame) // frame is present { if (compressed) images[group][frame] = new CompImage(file, frame, group); else images[group][frame] = new SDLImage(file, frame, group); return true; } } // still here? image is missing printError(frame, group, "LoadFrame"); images[group][frame] = new SDLImage("DEFAULT", compressed); } else //load from separate file { std::string filename = source[group][frame].Struct().find("file")->second.String(); IImage * img = getFromExtraDef(filename); if (!img) img = new SDLImage(filename, compressed); images[group][frame] = img; return true; } return false; } bool CAnimation::unloadFrame(size_t frame, size_t group) { IImage *image = getImage(frame, group, false); if (image) { //decrease ref count for image and delete if needed if (image->decreaseRef()) { delete 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(); for(const JsonNode &group : config["sequences"].Vector()) { size_t groupID = group["group"].Float();//TODO: string-to-value conversion("moving" -> MOVING) source[groupID].clear(); for(const JsonNode &frame : group["frames"].Vector()) { source[groupID].push_back(JsonNode()); std::string filename = frame.String(); source[groupID].back()["file"].String() = basepath + filename; } } for(const JsonNode &node : config["images"].Vector()) { size_t group = node["group"].Float(); size_t frame = node["frame"].Float(); if (source[group].size() <= frame) source[group].resize(frame+1); source[group][frame] = node; std::string filename = node["file"].String(); source[group][frame]["file"].String() = basepath + filename; } } void CAnimation::init(CDefFile * file) { if (file) { const std::map defEntries = file->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); } } CDefFile * CAnimation::getFile() const { ResourceID identifier(std::string("SPRITES/") + name, EResType::ANIMATION); if (CResourceHandler::get()->existsResource(identifier)) return new CDefFile(name); return nullptr; } void CAnimation::printError(size_t frame, size_t group, std::string type) const { logGlobal->errorStream() << type <<" error: Request for frame not present in CAnimation! " <<"\tFile name: "<warnStream()<<"Warning: not all frames were unloaded from "<second.find(frame); if (imageIter != groupIter->second.end()) return imageIter->second; } if (verbose) printError(frame, group, "GetImage"); return nullptr; } void CAnimation::load() { CDefFile * file = getFile(); for (auto & elem : source) for (size_t image=0; image < elem.second.size(); image++) loadFrame(file, image, elem.first); delete file; } void CAnimation::unload() { for (auto & elem : source) for (size_t image=0; image < elem.second.size(); image++) unloadFrame(image, elem.first); } void CAnimation::loadGroup(size_t group) { CDefFile * file = getFile(); if (vstd::contains(source, group)) for (size_t image=0; image < source[group].size(); image++) loadFrame(file, image, group); delete file; } 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) { CDefFile * file = getFile(); loadFrame(file, frame, group); delete file; } 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; } std::set CAnimation::loadedAnims; void CAnimation::getAnimInfo() { logGlobal->errorStream()<<"Animation stats: Loaded "<errorStream()<<"Name: "<name<<" Groups: "<images.size(); if (!anim->images.empty()) logGlobal->errorStream()<<", "<images.begin()->second.size()<<" image loaded in group "<< anim->images.begin()->first; } } 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() : fadingSurface(nullptr), fading(false), fadingMode(EMode::NONE) { } CFadeAnimation::~CFadeAnimation() { if (fadingSurface && shouldFreeSurface) SDL_FreeSurface(fadingSurface); } void CFadeAnimation::init(EMode mode, SDL_Surface * sourceSurface, bool freeSurfaceAtEnd /* = false */, float animDelta /* = DEFAULT_DELTA */) { 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->warnStream() << "Tried to init fading animation that is already running."; if (fadingSurface && shouldFreeSurface) SDL_FreeSurface(fadingSurface); } if (animDelta <= 0.0f) { logGlobal->warnStream() << "Fade anim: delta should be positive; " << animDelta << " given."; 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.98/client/gui/CAnimation.h000066400000000000000000000155131250671757600166720ustar00rootroot00000000000000#pragma once #include "../../lib/vcmi_endian.h" #include "gui/Geometries.h" #include "../../lib/GameConstants.h" /* * 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 * */ struct SDL_Surface; class SDLImageLoader; class CompImageLoader; class JsonNode; /// Class for def loading, methods are based on CDefHandler /// 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; ui8 * data; SDL_Color * 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; }; /* * Base class for images, can be used for non-animation pictures as well */ class IImage { int refCount; public: //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; //decrease ref count, returns true if image can be deleted (refCount <= 0) bool decreaseRef(); void increaseRef(); //Change palette to specific player virtual void playerColored(PlayerColor player)=0; virtual int width() const=0; virtual int height() const=0; IImage(); virtual ~IImage() {}; }; /* * 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, bool compressed=false); //Load from bitmap file SDLImage(std::string filename, bool compressed=false); //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; void playerColored(PlayerColor player); int width() const; int height() const; friend class SDLImageLoader; }; /* * RLE-compressed image data for 8-bit images with alpha-channel, currently far from finished * primary purpose is not high compression ratio but fast drawing. * Consist of repeatable segments with format similar to H3 def compression: * 1st byte: * if (byte == 0xff) * raw data, opaque and semi-transparent data always in separate blocks * else * RLE-compressed image data with this color * 2nd byte = size of segment * raw data (if any) */ class CompImage : public IImage { //x,y - margins, w,h - sprite size Rect sprite; //total size including borders Point fullSize; //RLE-d data ui8 * surf; //array of offsets for each line ui32 * line; //palette SDL_Color *palette; //Used internally to blit one block of data template void BlitBlock(ui8 type, ui8 size, ui8 *&data, ui8 *&dest, ui8 alpha) const; void BlitBlockWithBpp(ui8 bpp, ui8 type, ui8 size, ui8 *&data, ui8 *&dest, ui8 alpha, bool rotated) const; public: //Load image from def file CompImage(const CDefFile *data, size_t frame, size_t group=0); //TODO: load image from SDL_Surface CompImage(SDL_Surface * surf); ~CompImage(); void draw(SDL_Surface *where, int posX=0, int posY=0, Rect *src=nullptr, ui8 alpha=255) const; void playerColored(PlayerColor player); int width() const; int height() const; friend class CompImageLoader; }; /// 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; //if true all frames will be stored in compressed (RLE) state const bool compressed; //loader, will be called by load(), require opened def file for loading from it. Returns true if image is loaded bool loadFrame(CDefFile * file, 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(CDefFile * file); //try to open def file CDefFile * getFile() const; //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 IImage * getFromExtraDef(std::string filename); public: CAnimation(std::string Name, bool Compressed = false); CAnimation(); ~CAnimation(); //static method for debugging - print info about loaded animations static void getAnimInfo(); static std::set loadedAnims; //add custom surface to the selected position. void setCustom(std::string filename, size_t frame, size_t group=0); //get pointer to image from specific group, nullptr if not found IImage * getImage(size_t frame, size_t group=0, bool verbose=true) const; //all available frames void load (); void unload(); //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; }; 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.98/client/gui/CCursorHandler.cpp000066400000000000000000000107121250671757600200550ustar00rootroot00000000000000#include "StdInc.h" #include "CCursorHandler.h" #include #include "SDL_Extensions.h" #include "CGuiHandler.h" #include "widgets/Images.h" #include "../CMT.h" /* * 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 * */ void CCursorHandler::initCursor() { xpos = ypos = 0; type = ECursor::DEFAULT; dndObject = nullptr; currentCursor = nullptr; help = CSDL_Ext::newSurface(40,40); #ifndef VCMI_SDL1 //No blending. Ensure, that we are copying pixels during "screen restore draw" SDL_SetSurfaceBlendMode(help,SDL_BLENDMODE_NONE); #endif // VCMI_SDL1 SDL_ShowCursor(SDL_DISABLE); changeGraphic(ECursor::ADVENTURE, 0); } void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index) { std::string cursorDefs[4] = { "CRADVNTR.DEF", "CRCOMBAT.DEF", "CRDEFLT.DEF", "CRSPELL.DEF" }; if (type != this->type) { BLOCK_CAPTURING; // not used here this->type = type; this->frame = index; delete currentCursor; currentCursor = new CAnimImage(cursorDefs[int(type)], index); } if (frame != index) { frame = index; currentCursor->setFrame(index); } } void CCursorHandler::dragAndDropCursor(CAnimImage * object) { if (dndObject) delete dndObject; dndObject = object; } void CCursorHandler::cursorMove(const int & x, const int & y) { xpos = x; ypos = y; } void CCursorHandler::drawWithScreenRestore() { if(!showing) return; int x = xpos, y = ypos; shiftPos(x, y); SDL_Rect temp_rect1 = genRect(40,40,x,y); SDL_Rect temp_rect2 = genRect(40,40,0,0); SDL_BlitSurface(screen, &temp_rect1, help, &temp_rect2); if (dndObject) { dndObject->moveTo(Point(x - dndObject->pos.w/2, y - dndObject->pos.h/2)); dndObject->showAll(screen); } else { currentCursor->moveTo(Point(x,y)); currentCursor->showAll(screen); } } void CCursorHandler::drawRestored() { if(!showing) return; int x = xpos, y = ypos; shiftPos(x, y); SDL_Rect temp_rect = genRect(40, 40, x, y); SDL_BlitSurface(help, nullptr, screen, &temp_rect); //blitAt(help,x,y); } void CCursorHandler::draw(SDL_Surface *to) { currentCursor->moveTo(Point(xpos, ypos)); currentCursor->showAll(screen); } 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() { drawWithScreenRestore(); CSDL_Ext::update(screen); drawRestored(); } CCursorHandler::~CCursorHandler() { if(help) SDL_FreeSurface(help); delete currentCursor; delete dndObject; } vcmi-0.98/client/gui/CCursorHandler.h000066400000000000000000000037401250671757600175250ustar00rootroot00000000000000#pragma once class CAnimImage; struct SDL_Surface; /* * 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 * */ 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 { SDL_Surface * help; CAnimImage * currentCursor; CAnimImage * dndObject; //if set, overrides currentCursor bool showing; /// Draw cursor preserving original image below cursor void drawWithScreenRestore(); /// Restore original image below cursor void drawRestored(); /// Simple draw cursor void draw(SDL_Surface *to); 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 (CAnimImage * image); void render(); void shiftPos( int &x, int &y ); void hide() { showing=0; }; void show() { showing=1; }; /// change cursor's positions to (x, y) void cursorMove(const int & x, const int & y); /// Move cursor to screen center void centerCursor(); ~CCursorHandler(); }; vcmi-0.98/client/gui/CGuiHandler.cpp000066400000000000000000000352741250671757600173360ustar00rootroot00000000000000#include "StdInc.h" #include "CGuiHandler.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" 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::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); #ifndef VCMI_SDL1 processList(CIntObject::TEXTINPUT,activityFlag,&textInterested,cb); #endif // VCMI_SDL1 } 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( IShowActivatable *top ) { assert(listInt.front() == top); top->deactivate(); listInt.pop_front(); objsToBlit -= top; if(!listInt.empty()) listInt.front()->activate(); totalRedraw(); } void CGuiHandler::popIntTotally( IShowActivatable *top ) { assert(listInt.front() == top); popInt(top); delete top; fakeMouseMove(); } void CGuiHandler::pushInt( IShowActivatable *newInt ) { assert(newInt); assert(boost::range::find(listInt, newInt) == listInt.end()); // 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); newInt->activate(); objsToBlit.push_back(newInt); totalRedraw(); } 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(); delete listInt.front(); listInt.pop_front(); } if(!listInt.empty()) { listInt.front()->activate(); totalRedraw(); } fakeMouseMove(); } IShowActivatable * CGuiHandler::topInt() { if(listInt.empty()) return nullptr; 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()) { SDL_Event ev = events.front(); events.pop(); this->handleEvent(&ev); } } void CGuiHandler::handleEvent(SDL_Event *sEvent) { current = sEvent; bool prev; if (sEvent->type==SDL_KEYDOWN || sEvent->type==SDL_KEYUP) { SDL_KeyboardEvent key = sEvent->key; //translate numpad keys if(key.keysym.sym == SDLK_KP_ENTER) { key.keysym.sym = (SDLKey)SDLK_RETURN; #ifndef VCMI_SDL1 key.keysym.scancode = SDL_SCANCODE_RETURN; #endif // VCMI_SDL1 } bool keysCaptured = false; for(auto i=keyinterested.begin(); i != keyinterested.end() && current; i++) { if((*i)->captureThisEvent(key)) { keysCaptured = true; break; } } std::list miCopy = keyinterested; for(auto i=miCopy.begin(); i != miCopy.end() && current; i++) if(vstd::contains(keyinterested,*i) && (!keysCaptured || (*i)->captureThisEvent(key))) (**i).keyPressed(key); } else if(sEvent->type==SDL_MOUSEMOTION) { CCS->curh->cursorMove(sEvent->motion.x, sEvent->motion.y); handleMouseMotion(sEvent); } else if (sEvent->type==SDL_MOUSEBUTTONDOWN) { if(sEvent->button.button == SDL_BUTTON_LEFT) { if(lastClick == sEvent->motion && (SDL_GetTicks() - lastClickTime) < 300) { std::list hlp = doubleClickInterested; for(auto i=hlp.begin(); i != hlp.end() && current; i++) { if(!vstd::contains(doubleClickInterested,*i)) continue; if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y)) { (*i)->onDoubleClick(); } } } lastClick = sEvent->motion; lastClickTime = SDL_GetTicks(); std::list hlp = lclickable; for(auto i=hlp.begin(); i != hlp.end() && current; i++) { if(!vstd::contains(lclickable,*i)) continue; if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y)) { prev = (*i)->pressedL; (*i)->pressedL = true; (*i)->clickLeft(true, prev); } } } else if (sEvent->button.button == SDL_BUTTON_RIGHT) { std::list hlp = rclickable; for(auto i=hlp.begin(); i != hlp.end() && current; i++) { if(!vstd::contains(rclickable,*i)) continue; if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y)) { prev = (*i)->pressedR; (*i)->pressedR = true; (*i)->clickRight(true, prev); } } } #ifdef VCMI_SDL1 //SDL1x only events else if(sEvent->button.button == SDL_BUTTON_WHEELDOWN || sEvent->button.button == SDL_BUTTON_WHEELUP) { std::list hlp = wheelInterested; for(auto i=hlp.begin(); i != hlp.end() && current; i++) { if(!vstd::contains(wheelInterested,*i)) continue; (*i)->wheelScrolled(sEvent->button.button == SDL_BUTTON_WHEELDOWN, isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y)); } } #endif } #ifndef VCMI_SDL1 //SDL2x only events else if (sEvent->type == SDL_MOUSEWHEEL) { std::list hlp = wheelInterested; for(auto i=hlp.begin(); i != hlp.end() && current; i++) { if(!vstd::contains(wheelInterested,*i)) continue; (*i)->wheelScrolled(sEvent->wheel.y < 0, isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y)); } } else if(sEvent->type == SDL_TEXTINPUT) { for(auto it : textInterested) { it->textInputed(sEvent->text); } } else if(sEvent->type == SDL_TEXTEDITING) { for(auto it : textInterested) { it->textEdited(sEvent->edit); } } //todo: muiltitouch #endif // VCMI_SDL1 else if ((sEvent->type==SDL_MOUSEBUTTONUP) && (sEvent->button.button == SDL_BUTTON_LEFT)) { std::list hlp = lclickable; for(auto i=hlp.begin(); i != hlp.end() && current; i++) { if(!vstd::contains(lclickable,*i)) continue; prev = (*i)->pressedL; (*i)->pressedL = false; if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y)) { (*i)->clickLeft(false, prev); } else (*i)->clickLeft(boost::logic::indeterminate, prev); } } else if ((sEvent->type==SDL_MOUSEBUTTONUP) && (sEvent->button.button == SDL_BUTTON_RIGHT)) { std::list hlp = rclickable; for(auto i=hlp.begin(); i != hlp.end() && current; i++) { if(!vstd::contains(rclickable,*i)) continue; prev = (*i)->pressedR; (*i)->pressedR = false; if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y)) { (*i)->clickRight(false, prev); } else (*i)->clickRight(boost::logic::indeterminate, prev); } } current = nullptr; } //event end void CGuiHandler::handleMouseMotion(SDL_Event *sEvent) { //sending active, hovered hoverable objects hover() call std::vector hlp; for(auto & elem : hoverable) { if (isItIn(&(elem)->pos,sEvent->motion.x,sEvent->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(sEvent->motion); } void CGuiHandler::simpleRedraw() { //update only top interface and draw background if(objsToBlit.size() > 1) blitAt(screen2,0,0,screen); //blit background 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 || isItIn(&(elem)->pos, motion.x, motion.y)) { (elem)->mouseMoved(motion); } } } void CGuiHandler::fakeMouseMove() { SDL_Event evnt; #ifdef VCMI_SDL1 SDL_MouseMotionEvent sme = {SDL_MOUSEMOTION, 0, 0, 0, 0, 0, 0}; #else SDL_MouseMotionEvent sme = {SDL_MOUSEMOTION, 0, 0, 0, 0, 0, 0, 0, 0}; #endif int x, y; sme.state = SDL_GetMouseState(&x, &y); sme.x = x; sme.y = y; evnt.motion = sme; current = &evnt; handleMouseMotion(&evnt); } void CGuiHandler::renderFrame() { auto doUpdate = [this]() { if(nullptr != curInt) { curInt -> update(); } // draw the mouse cursor and update the screen CCS->curh->render(); #ifndef VCMI_SDL1 if(0 != SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr)) logGlobal->errorStream() << __FUNCTION__ << " SDL_RenderCopy " << SDL_GetError(); SDL_RenderPresent(mainRenderer); #endif }; if(curInt) curInt->runLocked(doUpdate); else doUpdate(); mainFPSmng->framerateDelay(); // holds a constant FPS } CGuiHandler::CGuiHandler() :lastClick(-500, -500) { 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 } CGuiHandler::~CGuiHandler() { delete mainFPSmng; } void CGuiHandler::breakEventHandling() { current = nullptr; } 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)); } SDLKey CGuiHandler::arrowToNum( SDLKey key ) { #ifdef VCMI_SDL1 switch(key) { case SDLK_DOWN: return SDLK_KP2; case SDLK_UP: return SDLK_KP8; case SDLK_LEFT: return SDLK_KP4; case SDLK_RIGHT: return SDLK_KP6; default: throw std::runtime_error("Wrong key!");assert(0); } #else 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!"); } #endif // 0 } SDLKey CGuiHandler::numToDigit( SDLKey key ) { #ifdef VCMI_SDL1 if(key >= SDLK_KP0 && key <= SDLK_KP9) return SDLKey(key - SDLK_KP0 + SDLK_0); #endif // 0 #define REMOVE_KP(keyName) case SDLK_KP_ ## keyName : return SDLK_ ## keyName; switch(key) { #ifndef VCMI_SDL1 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) #endif // VCMI_SDL1 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( SDLKey key, bool number ) { #ifdef VCMI_SDL1 if(number) return key >= SDLK_KP0 && key <= SDLK_KP9; else return key >= SDLK_KP0 && key <= SDLK_KP_EQUALS; #else 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; #endif // 0 } bool CGuiHandler::isArrowKey( SDLKey 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; } 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); } currentTicks = SDL_GetTicks(); fps = ceil(1000.0 / timeElapsed); // 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.98/client/gui/CGuiHandler.h000066400000000000000000000116241250671757600167740ustar00rootroot00000000000000#pragma once //#include "../../lib/CStopWatch.h" #include "Geometries.h" #include "SDL_Extensions.h" class CFramerateManager; class CGStatusBar; class CIntObject; class IUpdateable; class ILockedUpdatable; class IShowActivatable; class IShowable; /* * 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 * */ // A fps manager which holds game updates at a constant rate class CFramerateManager { private: double rateticks; ui32 lastticks, timeElapsed; int rate; 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) CGStatusBar * statusbar; private: typedef std::list CIntObjectList; //active GUI elements (listening for events CIntObjectList lclickable, rclickable, hoverable, keyinterested, motioninterested, timeinterested, wheelInterested, doubleClickInterested; #ifndef VCMI_SDL1 CIntObjectList textInterested; #endif // VCMI_SDL1 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 ILockedUpdatable *curInt; Point lastClick; unsigned lastClickTime; 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 popInt(IShowActivatable *top); //removes given interface from the top and activates next void popIntTotally(IShowActivatable *top); //deactivates, deletes, removes given interface from the top and activates next void pushInt(IShowActivatable *newInt); //deactivate old top interface, activates this one and pushes to the top void popInts(int howMany); //pops one or more interfaces - deactivates top, deletes and removes given number of interfaces, activates new front IShowActivatable *topInt(); //returns top interface void updateTime(); //handles timeInterested void handleEvents(); //takes events from queue and calls interested objects void handleEvent(SDL_Event *sEvent); void handleMouseMotion(SDL_Event *sEvent); 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 ui8 defActionsDef; //default auto actions ui8 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 static SDLKey arrowToNum(SDLKey key); //converts arrow key to according numpad key static SDLKey numToDigit(SDLKey key);//converts numpad digit key to normal digit key static bool isNumKey(SDLKey key, bool number = true); //checks if key is on numpad (numbers - check only for numpad digits) static bool isArrowKey(SDLKey key); static bool amIGuiThread(); static void pushSDLEvent(int type, int usercode = 0); }; extern CGuiHandler GH; //global gui handler template void pushIntT() { GH.pushInt(new T()); } struct SObjectConstruction { CIntObject *myObj; SObjectConstruction(CIntObject *obj); ~SObjectConstruction(); }; struct SSetCaptureState { bool previousCapture; ui8 prevActions; SSetCaptureState(bool allow, ui8 actions); ~SSetCaptureState(); }; #define OBJ_CONSTRUCTION SObjectConstruction obj__i(this) #define OBJ_CONSTRUCTION_CAPTURING_ALL defActions = 255; SSetCaptureState obj__i1(true, 255); SObjectConstruction obj__i(this) #define BLOCK_CAPTURING SSetCaptureState obj__i(false, 0) #define BLOCK_CAPTURING_DONT_TOUCH_REC_ACTIONS SSetCaptureState obj__i(false, GH.defActionsDef) vcmi-0.98/client/gui/CIntObject.cpp000066400000000000000000000173731250671757600171750ustar00rootroot00000000000000#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) { pressedL = pressedR = 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); } 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->warnStream() << "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); } CIntObject::~CIntObject() { if (active_m) deactivate(); if(defActions & DISPOSE) { while (!children.empty()) if(children.front()->recActions & DISPOSE) delete children.front(); else removeChild(children.front()); } if(parent_m) parent_m->removeChild(this); } void CIntObject::printAtLoc( const std::string & text, int x, int y, EFonts font, SDL_Color kolor/*=Colors::WHITE*/, SDL_Surface * dst/*=screen*/ ) { graphics->fonts[font]->renderTextLeft(dst, text, kolor, Point(pos.x + x, pos.y + y)); } void CIntObject::printAtRightLoc( const std::string & text, int x, int y, EFonts font, SDL_Color kolor/*=Colors::WHITE*/, SDL_Surface * dst/*=screen*/ ) { graphics->fonts[font]->renderTextRight(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/*=Colors::WHITE*/, SDL_Surface * dst/*=screen*/ ) { 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::printToLoc( const std::string & text, int x, int y, EFonts font, SDL_Color kolor, SDL_Surface * dst ) { graphics->fonts[font]->renderTextRight(dst, text, 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->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 /*= true*/ ) { 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 /*= true*/ ) { moveBy(Point(p.x - pos.x, p.y - pos.y), propagate); } void CIntObject::addChild(CIntObject *child, bool adjustPosition /*= false*/) { 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 /*= false*/) { if (!child) return; assert(vstd::contains(children, child)); assert(child->parent_m == this); children -= child; child->parent_m = nullptr; if(adjustPosition) child->pos -= pos; } void CIntObject::drawBorderLoc(SDL_Surface * sur, const Rect &r, const int3 &color) { CSDL_Ext::drawBorder(sur, r + pos, color); } 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 /*= true*/) { 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 = pressedL; if(key.state == SDL_PRESSED) { pressedL = true; clickLeft(true, prev); } else { pressedL = false; clickLeft(false, prev); } } } vcmi-0.98/client/gui/CIntObject.h000066400000000000000000000172001250671757600166270ustar00rootroot00000000000000/* * 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 CPicture; class CGuiHandler; 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(){}; //d-tor }; class IUpdateable { public: virtual void update()=0; virtual ~IUpdateable(){}; //d-tor }; class ILockedUpdatable: public IUpdateable { public: virtual void runLocked(std::function cb) = 0; virtual ~ILockedUpdatable(){}; //d-tor }; // 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(){}; //d-tor }; 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(){}; //d-tor }; //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; 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; //FIXME: workaround void deactivateKeyboard() { deactivate(KEYBOARD); } /* * 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(); //d-tor //l-clicks handling /*const*/ bool pressedL; //for determining if object is L-pressed virtual void clickLeft(tribool down, bool previousState){} //r-clicks handling /*const*/ bool pressedR; //for determining if object is R-pressed virtual void clickRight(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) #ifndef VCMI_SDL1 virtual void textInputed(const SDL_TextInputEvent & event){}; virtual void textEdited(const SDL_TextEditingEvent & event){}; #endif // VCMI_SDL1 //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, 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(); void deactivate(); //called each frame to update screen void show(SDL_Surface * to); //called on complete redraw only void showAll(SDL_Surface * to); //request complete redraw of this object void redraw(); 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 */ //wrappers for CSDL_Ext methods. This versions use coordinates relative to pos void drawBorderLoc(SDL_Surface * sur, const Rect &r, const int3 &color); //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 printToLoc(const std::string & text, int x, int y, EFonts font, SDL_Color color, SDL_Surface * dst); void printAtRightLoc(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); //call-in }; vcmi-0.98/client/gui/Fonts.cpp000066400000000000000000000264301250671757600162740ustar00rootroot00000000000000#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" /* * 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 * */ 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.98/client/gui/Fonts.h000066400000000000000000000106361250671757600157420ustar00rootroot00000000000000#pragma once /* * 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 * */ 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.98/client/gui/Geometries.cpp000066400000000000000000000010421250671757600172760ustar00rootroot00000000000000#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 /*= 1*/) /*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.98/client/gui/Geometries.h000066400000000000000000000123121250671757600167450ustar00rootroot00000000000000#pragma once #include #include "../../lib/int3.h" /* * 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 * */ #ifdef max #undef max #endif #ifdef min #undef min #endif 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) //c-tor { x = X; y = Y; w = W; h = H; } Rect(const Point & position, const Point & size) //c-tor { x = position.x; y = position.y; w = size.x; h = size.y; } Rect(const SDL_Rect & r) //c-tor { 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.98/client/gui/SDL_Compat.h000066400000000000000000000012371250671757600165730ustar00rootroot00000000000000#pragma once /* * 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 * */ #include #if (SDL_MAJOR_VERSION == 2) #define VCMI_SDL2 #include typedef int SDLX_Coord; typedef int SDLX_Size; typedef SDL_Keycode SDLKey; #define SDL_SRCCOLORKEY SDL_TRUE #define SDL_FULLSCREEN SDL_WINDOW_FULLSCREEN #elif (SDL_MAJOR_VERSION == 1) #define VCMI_SDL1 //SDL 1.x typedef Sint16 SDLX_Coord; typedef Uint16 SDLX_Size; #else #error "unknown or unsupported SDL version" #endif vcmi-0.98/client/gui/SDL_Extensions.cpp000066400000000000000000000732711250671757600200510ustar00rootroot00000000000000#include "StdInc.h" #include "SDL_Extensions.h" #include "SDL_Pixels.h" #include "../CGameInfo.h" #include "../CMessage.h" #include "../CDefHandler.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::DEFAULT_KEY_COLOR = {0, 255, 255, 0}; #if (SDL_MAJOR_VERSION == 2) 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->errorStream() << __FUNCTION__ << "SDL_UpdateTexture " << SDL_GetError(); SDL_RenderClear(mainRenderer); if(0 != SDL_RenderCopy(mainRenderer, screenTexture, NULL, NULL)) logGlobal->errorStream() << __FUNCTION__ << "SDL_RenderCopy " << SDL_GetError(); SDL_RenderPresent(mainRenderer); } #endif // VCMI_SDL1 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(mod->flags,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( SDL_SWSURFACE, 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); } 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); const int bpp = ret->format->BytesPerPixel; char * src = reinterpret_cast(toRot->pixels); char * dst = reinterpret_cast(ret->pixels); for(int i=0; ih; i++) { char * srcPxl = src; char * dstPxl = dst + ret->w * bpp; if (bpp == 1) { // much faster for 8-bit surfaces (majority of our data) std::reverse_copy(src, src + ret->pitch, dst); } else { for(int j=0; jw; j++) { dstPxl -= bpp; std::copy(srcPxl, srcPxl + bpp, dstPxl); srcPxl += bpp; } } src += toRot->pitch; dst += ret->pitch; } return ret; } // Horizontal flip SDL_Surface * CSDL_Ext::horizontalFlip(SDL_Surface * toRot) { SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); 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; } 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_SRCCOLORKEY, 0); } static void prepareOutRect(SDL_Rect *src, SDL_Rect *dst, const SDL_Rect & clip_rect) { const int xoffset = std::max(clip_rect.x - dst->x, 0), yoffset = std::max(clip_rect.y - dst->y, 0); src->x += xoffset; src->y += yoffset; dst->x += xoffset; dst->y += yoffset; src->w = dst->w = std::max(0,std::min(dst->w - xoffset, clip_rect.x + clip_rect.w - dst->x)); src->h = dst->h = std::max(0,std::min(dst->h - yoffset, clip_rect.y + clip_rect.h - dst->y)); } template void CSDL_Ext::blitWithRotateClip(SDL_Surface *src,SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect, ui8 rotation)//srcRect is not used, works with 8bpp sources and 24bpp dests { if(!rotation) { CSDL_Ext::blitSurface(src, srcRect, dst, dstRect); } else { static void (*blitWithRotate[])(const SDL_Surface *, const SDL_Rect *, SDL_Surface *, const SDL_Rect *) = {blitWithRotate1, blitWithRotate2, blitWithRotate3}; prepareOutRect(srcRect, dstRect, dst->clip_rect); blitWithRotate[rotation-1](src, srcRect, dst, dstRect); } } template void CSDL_Ext::blitWithRotateClipVal( SDL_Surface *src,SDL_Rect srcRect, SDL_Surface * dst, SDL_Rect dstRect, ui8 rotation ) { blitWithRotateClip(src, &srcRect, dst, &dstRect, rotation); } template void CSDL_Ext::blitWithRotateClipWithAlpha(SDL_Surface *src,SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect, ui8 rotation)//srcRect is not used, works with 8bpp sources and 24bpp dests { if(!rotation) { blit8bppAlphaTo24bpp(src, srcRect, dst, dstRect); } else { static void (*blitWithRotate[])(const SDL_Surface *, const SDL_Rect *, SDL_Surface *, const SDL_Rect *) = {blitWithRotate1WithAlpha, blitWithRotate2WithAlpha, blitWithRotate3WithAlpha}; prepareOutRect(srcRect, dstRect, dst->clip_rect); blitWithRotate[rotation-1](src, srcRect, dst, dstRect); } } template void CSDL_Ext::blitWithRotateClipValWithAlpha( SDL_Surface *src,SDL_Rect srcRect, SDL_Surface * dst, SDL_Rect dstRect, ui8 rotation ) { blitWithRotateClipWithAlpha(src, &srcRect, dst, &dstRect, rotation); } template void CSDL_Ext::blitWithRotate1(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect)//srcRect is not used, works with 8bpp sources and 24/32 bpp dests { Uint8 *sp = getPxPtr(src, src->w - srcRect->w - srcRect->x, srcRect->y); Uint8 *dporg = (Uint8 *)dst->pixels + dstRect->y*dst->pitch + (dstRect->x+dstRect->w)*bpp; const SDL_Color * const colors = src->format->palette->colors; for(int i=dstRect->h; i>0; i--, dporg += dst->pitch) { Uint8 *dp = dporg; for(int j=dstRect->w; j>0; j--, sp++) ColorPutter::PutColor(dp, colors[*sp]); sp += src->w - dstRect->w; } } template void CSDL_Ext::blitWithRotate2(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect)//srcRect is not used, works with 8bpp sources and 24/32 bpp dests { Uint8 *sp = getPxPtr(src, srcRect->x, src->h - srcRect->h - srcRect->y); Uint8 *dporg = (Uint8 *)dst->pixels + (dstRect->y + dstRect->h - 1)*dst->pitch + dstRect->x*bpp; const SDL_Color * const colors = src->format->palette->colors; for(int i=dstRect->h; i>0; i--, dporg -= dst->pitch) { Uint8 *dp = dporg; for(int j=dstRect->w; j>0; j--, sp++) ColorPutter::PutColor(dp, colors[*sp]); sp += src->w - dstRect->w; } } template void CSDL_Ext::blitWithRotate3(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect)//srcRect is not used, works with 8bpp sources and 24/32 bpp dests { Uint8 *sp = (Uint8 *)src->pixels + (src->h - srcRect->h - srcRect->y)*src->pitch + (src->w - srcRect->w - srcRect->x); Uint8 *dporg = (Uint8 *)dst->pixels +(dstRect->y + dstRect->h - 1)*dst->pitch + (dstRect->x+dstRect->w)*bpp; const SDL_Color * const colors = src->format->palette->colors; for(int i=dstRect->h; i>0; i--, dporg -= dst->pitch) { Uint8 *dp = dporg; for(int j=dstRect->w; j>0; j--, sp++) ColorPutter::PutColor(dp, colors[*sp]); sp += src->w - dstRect->w; } } template void CSDL_Ext::blitWithRotate1WithAlpha(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect)//srcRect is not used, works with 8bpp sources and 24/32 bpp dests { Uint8 *sp = (Uint8 *)src->pixels + srcRect->y*src->pitch + (src->w - srcRect->w - srcRect->x); Uint8 *dporg = (Uint8 *)dst->pixels + dstRect->y*dst->pitch + (dstRect->x+dstRect->w)*bpp; const SDL_Color * const colors = src->format->palette->colors; for(int i=dstRect->h; i>0; i--, dporg += dst->pitch) { Uint8 *dp = dporg; for(int j=dstRect->w; j>0; j--, sp++) { if(*sp) ColorPutter::PutColor(dp, colors[*sp]); else dp -= bpp; } sp += src->w - dstRect->w; } } template void CSDL_Ext::blitWithRotate2WithAlpha(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect)//srcRect is not used, works with 8bpp sources and 24/32 bpp dests { Uint8 *sp = (Uint8 *)src->pixels + (src->h - srcRect->h - srcRect->y)*src->pitch + srcRect->x; Uint8 *dporg = (Uint8 *)dst->pixels + (dstRect->y + dstRect->h - 1)*dst->pitch + dstRect->x*bpp; const SDL_Color * const colors = src->format->palette->colors; for(int i=dstRect->h; i>0; i--, dporg -= dst->pitch) { Uint8 *dp = dporg; for(int j=dstRect->w; j>0; j--, sp++) { if(*sp) ColorPutter::PutColor(dp, colors[*sp]); else dp += bpp; } sp += src->w - dstRect->w; } } template void CSDL_Ext::blitWithRotate3WithAlpha(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect)//srcRect is not used, works with 8bpp sources and 24/32 bpp dests { Uint8 *sp = (Uint8 *)src->pixels + (src->h - srcRect->h - srcRect->y)*src->pitch + (src->w - srcRect->w - srcRect->x); Uint8 *dporg = (Uint8 *)dst->pixels +(dstRect->y + dstRect->h - 1)*dst->pitch + (dstRect->x+dstRect->w)*bpp; const SDL_Color * const colors = src->format->palette->colors; for(int i=dstRect->h; i>0; i--, dporg -= dst->pitch) { Uint8 *dp = dporg; for(int j=dstRect->w; j>0; j--, sp++) { if(*sp) ColorPutter::PutColor(dp, colors[*sp]); else dp -= bpp; } sp += src->w - dstRect->w; } } template int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect) { if (src && src->format->BytesPerPixel==1 && dst && (bpp==3 || bpp==4 || bpp==2)) //everything's ok { SDL_Rect fulldst; int srcx, srcy, w, h; /* 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 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 #ifdef VCMI_SDL1 ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, tbc.unused); #else ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, tbc.a); #endif // 0 } } 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->errorStream() << (int)dst->format->BitsPerPixel << " bpp is not supported!!!"; return -1; } } Uint32 CSDL_Ext::colorToUint32(const SDL_Color * color) { Uint32 ret = 0; #ifdef VCMI_SDL1 ret+=color->unused; #else ret+=color->a; #endif // 0 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) { #ifdef VCMI_SDL1 if(what) SDL_UpdateRect(what, 0, 0, what->w, what->h); #else if(!what) return; if(0 !=SDL_UpdateTexture(screenTexture, nullptr, what->pixels, what->pitch)) logGlobal->errorStream() << __FUNCTION__ << "SDL_UpdateTexture " << SDL_GetError(); #endif // VCMI_SDL1 } 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->warnStream() << "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->errorStream() << (int)dest->format->BitsPerPixel << "bpp is not supported!"; 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->errorStream() << (int)dest->format->BitsPerPixel << "bpp is not supported!"; 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; #ifdef VCMI_SDL1 SDL_GetRGBA(SDL_GetPixel(srf, x, y), srf->format, &color.r, &color.g, &color.b, &color.unused); #else SDL_GetRGBA(SDL_GetPixel(srf, x, y), srf->format, &color.r, &color.g, &color.b, &color.a); #endif // 0 // color is considered transparent here if // a) image has aplha: less than 50% transparency // b) no alpha: color is cyan if (srf->format->Amask) #ifdef VCMI_SDL1 return color.unused < 128; // almost transparent #else return color.a < 128; // almost transparent #endif // 0 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 /*= 255*/) { 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 /*= 255*/) { 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); } BlitterWithRotationVal CSDL_Ext::getBlitterWithRotation(SDL_Surface *dest) { switch(dest->format->BytesPerPixel) { case 2: return blitWithRotateClipVal<2>; case 3: return blitWithRotateClipVal<3>; case 4: return blitWithRotateClipVal<4>; default: logGlobal->errorStream() << (int)dest->format->BitsPerPixel << " bpp is not supported!!!"; break; } assert(0); return nullptr; } BlitterWithRotationVal CSDL_Ext::getBlitterWithRotationAndAlpha(SDL_Surface *dest) { switch(dest->format->BytesPerPixel) { case 2: return blitWithRotateClipValWithAlpha<2>; case 3: return blitWithRotateClipValWithAlpha<3>; case 4: return blitWithRotateClipValWithAlpha<4>; default: logGlobal->errorStream() << (int)dest->format->BitsPerPixel << " bpp is not supported!!!"; break; } assert(0); return nullptr; } 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, SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect ) { if (dst != screen) { SDL_BlitSurface(src, srcRect, dst, dstRect); } else { SDL_Rect betterDst; if (dstRect) { betterDst = *dstRect; } else { betterDst = Rect(0, 0, dst->w, dst->h); } SDL_BlitSurface(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) { #ifndef VCMI_SDL1 if (SDL_IsTextInputActive() == SDL_FALSE) { SDL_StartTextInput(); } SDL_SetTextInputRect(where); #endif } void CSDL_Ext::stopTextInput() { #ifndef VCMI_SDL1 if (SDL_IsTextInputActive() == SDL_TRUE) { SDL_StopTextInput(); } #endif } STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) { #ifdef VCMI_SDL1 return SDL_MapRGB(surface->format, color.r, color.g, color.b); #else return SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); #endif } void CSDL_Ext::setColorKey(SDL_Surface * surface, SDL_Color color) { uint32_t key = mapColor(surface,color); SDL_SetColorKey(surface, SDL_SRCCOLORKEY, 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_SRCCOLORKEY, 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.98/client/gui/SDL_Extensions.h000066400000000000000000000275541250671757600175210ustar00rootroot00000000000000/* * 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 #ifndef VCMI_SDL1 #include #endif #include #include #include "../../lib/int3.h" //#include "../Graphics.h" #include "Geometries.h" #include "../../lib/GameConstants.h" //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 #if SDL_VERSION_ATLEAST(1,3,0) #define SDL_GetKeyState SDL_GetKeyboardState #endif //SDL2 support #if (SDL_MAJOR_VERSION == 2) 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); #endif inline bool isCtrlKeyDown() { #ifdef VCMI_SDL1 return SDL_GetKeyState(nullptr)[SDLK_LCTRL] || SDL_GetKeyState(nullptr)[SDLK_RCTRL]; #else return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RCTRL]; #endif } inline bool isAltKeyDown() { #ifdef VCMI_SDL1 return SDL_GetKeyState(nullptr)[SDLK_LALT] || SDL_GetKeyState(nullptr)[SDLK_RALT]; #else return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LALT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RALT]; #endif } inline bool isShiftKeyDown() { #ifdef VCMI_SDL1 return SDL_GetKeyState(nullptr)[SDLK_LSHIFT] || SDL_GetKeyState(nullptr)[SDLK_RSHIFT]; #else return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LSHIFT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RSHIFT]; #endif } namespace CSDL_Ext { STRONG_INLINE void colorSetAlpha(SDL_Color & color, Uint8 alpha) { #ifdef VCMI_SDL1 color.unused = alpha; #else color.a = alpha; #endif } //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; #ifdef VCMI_SDL1 dest.unused = source.unused; #else dest.a = source.a; #endif } inline void setAlpha(SDL_Surface * bg, int value) { #ifdef VCMI_SDL1 SDL_SetAlpha(bg, SDL_SRCALPHA, value); #else SDL_SetSurfaceAlphaMod(bg, value); #endif } } 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); /** * 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; /** 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 (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, 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 BlitterWithRotationVal getBlitterWithRotation(SDL_Surface *dest); BlitterWithRotationVal getBlitterWithRotationAndAlpha(SDL_Surface *dest); template void blitWithRotateClip(SDL_Surface *src,SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect, ui8 rotation);//srcRect is not used, works with 8bpp sources and 24bpp dests preserving clip_rect template void blitWithRotateClipVal(SDL_Surface *src,SDL_Rect srcRect, SDL_Surface * dst, SDL_Rect dstRect, ui8 rotation);//srcRect is not used, works with 8bpp sources and 24bpp dests preserving clip_rect template void blitWithRotateClipWithAlpha(SDL_Surface *src,SDL_Rect * srcRect, SDL_Surface * dst, SDL_Rect * dstRect, ui8 rotation);//srcRect is not used, works with 8bpp sources and 24bpp dests preserving clip_rect template void blitWithRotateClipValWithAlpha(SDL_Surface *src,SDL_Rect srcRect, SDL_Surface * dst, SDL_Rect dstRect, ui8 rotation);//srcRect is not used, works with 8bpp sources and 24bpp dests preserving clip_rect template void blitWithRotate1(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect);//srcRect is not used, works with 8bpp sources and 24bpp dests template void blitWithRotate2(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect);//srcRect is not used, works with 8bpp sources and 24bpp dests template void blitWithRotate3(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect);//srcRect is not used, works with 8bpp sources and 24bpp dests template void blitWithRotate1WithAlpha(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect);//srcRect is not used, works with 8bpp sources and 24bpp dests template void blitWithRotate2WithAlpha(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect);//srcRect is not used, works with 8bpp sources and 24bpp dests template void blitWithRotate3WithAlpha(const SDL_Surface *src, const SDL_Rect * srcRect, SDL_Surface * dst, const SDL_Rect * dstRect);//srcRect is not used, works with 8bpp sources and 24bpp dests 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.98/client/gui/SDL_Pixels.h000066400000000000000000000171011250671757600166110ustar00rootroot00000000000000#pragma once #include #include "SDL_Extensions.h" /* * 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 * */ // 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) { #ifdef VCMI_SDL1 PutColor(ptr, Color.r, Color.g, Color.b, Color.unused); #else PutColor(ptr, Color.r, Color.g, Color.b, Color.a); #endif } 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) { #ifdef VCMI_SDL1 PutColor(ptr, Color.r, Color.g, Color.b, Color.unused); #else PutColor(ptr, Color.r, Color.g, Color.b, Color.a); #endif } 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