2
0

ArmyManager.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. /*
  2. * BuildingManager.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "ArmyManager.h"
  12. #include "../Engine/Nullkiller.h"
  13. #include "../../../CCallback.h"
  14. #include "../../../lib/mapObjects/MapObjects.h"
  15. #include "../../../lib/mapping/CMapDefines.h"
  16. #include "../../../lib/IGameSettings.h"
  17. #include "../../../lib/GameConstants.h"
  18. #include "../../../lib/TerrainHandler.h"
  19. namespace NKAI
  20. {
  21. class StackUpgradeInfo
  22. {
  23. public:
  24. CreatureID initialCreature;
  25. CreatureID upgradedCreature;
  26. TResources cost;
  27. int count;
  28. uint64_t upgradeValue;
  29. StackUpgradeInfo(CreatureID initial, CreatureID upgraded, int count)
  30. :initialCreature(initial), upgradedCreature(upgraded), count(count)
  31. {
  32. cost = (upgradedCreature.toCreature()->getFullRecruitCost() - initialCreature.toCreature()->getFullRecruitCost()) * count;
  33. upgradeValue = (upgradedCreature.toCreature()->getAIValue() - initialCreature.toCreature()->getAIValue()) * count;
  34. }
  35. };
  36. void ArmyUpgradeInfo::addArmyToBuy(std::vector<SlotInfo> army)
  37. {
  38. for(auto slot : army)
  39. {
  40. resultingArmy.push_back(slot);
  41. upgradeValue += slot.power;
  42. upgradeCost += slot.creature->getFullRecruitCost() * slot.count;
  43. }
  44. }
  45. void ArmyUpgradeInfo::addArmyToGet(std::vector<SlotInfo> army)
  46. {
  47. for(auto slot : army)
  48. {
  49. resultingArmy.push_back(slot);
  50. upgradeValue += slot.power;
  51. }
  52. }
  53. std::vector<SlotInfo> ArmyManager::toSlotInfo(std::vector<creInfo> army) const
  54. {
  55. std::vector<SlotInfo> result;
  56. for(auto i : army)
  57. {
  58. SlotInfo slot;
  59. slot.creature = i.creID.toCreature();
  60. slot.count = i.count;
  61. slot.power = evaluateStackPower(i.creID.toCreature(), i.count);
  62. result.push_back(slot);
  63. }
  64. return result;
  65. }
  66. uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const
  67. {
  68. return howManyReinforcementsCanGet(hero, hero, source, ai->cb->getTile(hero->visitablePos())->getTerrainID());
  69. }
  70. std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const
  71. {
  72. const CCreatureSet * armies[] = { target, source };
  73. //we calculate total strength for each creature type available in armies
  74. std::map<const CCreature *, SlotInfo> creToPower;
  75. std::vector<SlotInfo> resultingArmy;
  76. for(auto armyPtr : armies)
  77. {
  78. for(auto & i : armyPtr->Slots())
  79. {
  80. auto cre = dynamic_cast<const CCreature*>(i.second->getType());
  81. auto & slotInfp = creToPower[cre];
  82. slotInfp.creature = cre;
  83. slotInfp.power += i.second->getPower();
  84. slotInfp.count += i.second->count;
  85. }
  86. }
  87. for(auto & pair : creToPower)
  88. resultingArmy.push_back(pair.second);
  89. boost::sort(resultingArmy, [](const SlotInfo & left, const SlotInfo & right) -> bool
  90. {
  91. return left.power > right.power;
  92. });
  93. return resultingArmy;
  94. }
  95. std::vector<SlotInfo>::iterator ArmyManager::getBestUnitForScout(std::vector<SlotInfo> & army, const TerrainId & armyTerrain) const
  96. {
  97. uint64_t totalPower = 0;
  98. for (const auto & unit : army)
  99. totalPower += unit.power;
  100. int baseMovementCost = cb->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
  101. bool terrainHasPenalty = armyTerrain.hasValue() && armyTerrain.toEntity(LIBRARY)->moveCost != baseMovementCost;
  102. // arbitrary threshold - don't give scout more than specified part of total AI value of our army
  103. uint64_t maxUnitValue = totalPower / 100;
  104. const auto & movementPointsLimits = cb->getSettings().getVector(EGameSettings::HEROES_MOVEMENT_POINTS_LAND);
  105. auto fastest = std::min_element(army.begin(), army.end(), [&](const SlotInfo & left, const SlotInfo & right) -> bool
  106. {
  107. uint64_t leftUnitPower = left.power / left.count;
  108. uint64_t rightUnitPower = left.power / left.count;
  109. bool leftUnitIsWeak = leftUnitPower < maxUnitValue || left.creature->getLevel() < 4;
  110. bool rightUnitIsWeak = rightUnitPower < maxUnitValue || right.creature->getLevel() < 4;
  111. if (leftUnitIsWeak != rightUnitIsWeak)
  112. return leftUnitIsWeak;
  113. if (terrainHasPenalty)
  114. {
  115. auto leftNativeTerrain = left.creature->getFactionID().toFaction()->nativeTerrain;
  116. auto rightNativeTerrain = right.creature->getFactionID().toFaction()->nativeTerrain;
  117. if (leftNativeTerrain != rightNativeTerrain)
  118. {
  119. if (leftNativeTerrain == armyTerrain)
  120. return true;
  121. if (rightNativeTerrain == armyTerrain)
  122. return false;
  123. }
  124. }
  125. int leftEffectiveMovement = std::min<int>(movementPointsLimits.size() - 1, left.creature->getMovementRange());
  126. int rightEffectiveMovement = std::min<int>(movementPointsLimits.size() - 1, right.creature->getMovementRange());
  127. int leftMovementPointsLimit = movementPointsLimits[leftEffectiveMovement];
  128. int rightMovementPointsLimit = movementPointsLimits[rightEffectiveMovement];
  129. if (leftMovementPointsLimit != rightMovementPointsLimit)
  130. return leftMovementPointsLimit > rightMovementPointsLimit;
  131. return leftUnitPower < rightUnitPower;
  132. });
  133. return fastest;
  134. }
  135. class TemporaryArmy : public CArmedInstance
  136. {
  137. public:
  138. void armyChanged() override {}
  139. TemporaryArmy()
  140. :CArmedInstance(nullptr, true)
  141. {
  142. }
  143. };
  144. std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source, const TerrainId & armyTerrain) const
  145. {
  146. auto sortedSlots = getSortedSlots(target, source);
  147. if(source->stacksCount() == 0)
  148. return sortedSlots;
  149. std::map<FactionID, uint64_t> alignmentMap;
  150. for(auto & slot : sortedSlots)
  151. {
  152. alignmentMap[slot.creature->getFactionID()] += slot.power;
  153. }
  154. std::set<FactionID> allowedFactions;
  155. std::vector<SlotInfo> resultingArmy;
  156. uint64_t armyValue = 0;
  157. TemporaryArmy newArmyInstance;
  158. while(allowedFactions.size() < alignmentMap.size())
  159. {
  160. auto strongestAlignment = vstd::maxElementByFun(alignmentMap, [&](std::pair<FactionID, uint64_t> pair) -> uint64_t
  161. {
  162. return vstd::contains(allowedFactions, pair.first) ? 0 : pair.second;
  163. });
  164. allowedFactions.insert(strongestAlignment->first);
  165. std::vector<SlotInfo> newArmy;
  166. uint64_t newValue = 0;
  167. newArmyInstance.clearSlots();
  168. for(auto & slot : sortedSlots)
  169. {
  170. if(vstd::contains(allowedFactions, slot.creature->getFactionID()))
  171. {
  172. auto slotID = newArmyInstance.getSlotFor(slot.creature->getId());
  173. if(slotID.validSlot())
  174. {
  175. newArmyInstance.setCreature(slotID, slot.creature->getId(), slot.count);
  176. newArmy.push_back(slot);
  177. }
  178. }
  179. }
  180. newArmyInstance.updateMoraleBonusFromArmy();
  181. for(auto & slot : newArmyInstance.Slots())
  182. {
  183. auto morale = slot.second->moraleVal();
  184. auto multiplier = 1.0f;
  185. const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
  186. const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
  187. if(morale < 0 && !badMoraleDice.empty())
  188. {
  189. size_t diceIndex = std::min<size_t>(badMoraleDice.size(), -morale) - 1;
  190. multiplier -= 1.0 / badMoraleDice.at(diceIndex);
  191. }
  192. else if(morale > 0 && !highMoraleDice.empty())
  193. {
  194. size_t diceIndex = std::min<size_t>(highMoraleDice.size(), morale) - 1;
  195. multiplier += 1.0 / highMoraleDice.at(diceIndex);
  196. }
  197. newValue += multiplier * slot.second->getPower();
  198. }
  199. if(armyValue >= newValue)
  200. {
  201. break;
  202. }
  203. resultingArmy = newArmy;
  204. armyValue = newValue;
  205. }
  206. if(resultingArmy.size() <= GameConstants::ARMY_SIZE
  207. && allowedFactions.size() == alignmentMap.size()
  208. && source->needsLastStack())
  209. {
  210. auto weakest = getBestUnitForScout(resultingArmy, armyTerrain);
  211. if(weakest->count == 1)
  212. {
  213. if (resultingArmy.size() == 1)
  214. logAi->warn("Unexpected resulting army size!");
  215. resultingArmy.erase(weakest);
  216. }
  217. else
  218. {
  219. weakest->power -= weakest->power / weakest->count;
  220. weakest->count--;
  221. }
  222. }
  223. return resultingArmy;
  224. }
  225. ui64 ArmyManager::howManyReinforcementsCanBuy(const CCreatureSet * h, const CGDwelling * t) const
  226. {
  227. return howManyReinforcementsCanBuy(h, t, ai->getFreeResources());
  228. }
  229. std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
  230. const CGDwelling * dwelling,
  231. TResources availableRes) const
  232. {
  233. std::vector<creInfo> creaturesInDwellings;
  234. auto army = std::make_shared<TemporaryArmy>();
  235. for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
  236. {
  237. auto ci = infoFromDC(dwelling->creatures[i]);
  238. if(!ci.count || ci.creID == CreatureID::NONE)
  239. continue;
  240. vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
  241. if(!ci.count)
  242. continue;
  243. SlotID dst = army->getFreeSlot();
  244. if(!dst.validSlot())
  245. break;
  246. army->setCreature(dst, ci.creID, ci.count);
  247. availableRes -= ci.creID.toCreature()->getFullRecruitCost() * ci.count;
  248. }
  249. return army;
  250. }
  251. ui64 ArmyManager::howManyReinforcementsCanBuy(
  252. const CCreatureSet * targetArmy,
  253. const CGDwelling * dwelling,
  254. const TResources & availableResources,
  255. uint8_t turn) const
  256. {
  257. ui64 aivalue = 0;
  258. auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources);
  259. for(const creInfo & ci : army)
  260. {
  261. aivalue += ci.count * ci.creID.toCreature()->getAIValue();
  262. }
  263. return aivalue;
  264. }
  265. std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const
  266. {
  267. return getArmyAvailableToBuy(hero, dwelling, ai->getFreeResources());
  268. }
  269. std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
  270. const CCreatureSet * hero,
  271. const CGDwelling * dwelling,
  272. TResources availableRes,
  273. uint8_t turn) const
  274. {
  275. std::vector<creInfo> creaturesInDwellings;
  276. int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount();
  277. bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7;
  278. const CGTownInstance * town = dwelling->ID == Obj::TOWN
  279. ? dynamic_cast<const CGTownInstance *>(dwelling)
  280. : nullptr;
  281. std::set<SlotID> alreadyDisbanded;
  282. for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
  283. {
  284. auto ci = infoFromDC(dwelling->creatures[i]);
  285. if(ci.creID == CreatureID::NONE) continue;
  286. if(i < GameConstants::CREATURES_PER_TOWN && countGrowth)
  287. {
  288. ci.count += town ? town->creatureGrowth(i) : ci.creID.toCreature()->getGrowth();
  289. }
  290. if(!ci.count) continue;
  291. // Calculate the market value of the new stack
  292. TResources newStackValue = ci.creID.toCreature()->getFullRecruitCost() * ci.count;
  293. SlotID dst = hero->getSlotFor(ci.creID);
  294. // Keep track of the least valuable slot in the hero's army
  295. SlotID leastValuableSlot;
  296. TResources leastValuableStackValue;
  297. leastValuableStackValue[6] = std::numeric_limits<int>::max();
  298. bool shouldDisband = false;
  299. if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
  300. {
  301. if(!freeHeroSlots) // No free slots; consider replacing
  302. {
  303. // Check for the least valuable existing stack
  304. for (auto& slot : hero->Slots())
  305. {
  306. if (alreadyDisbanded.find(slot.first) != alreadyDisbanded.end())
  307. continue;
  308. if(slot.second->getCreatureID() != CreatureID::NONE)
  309. {
  310. TResources currentStackValue = slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->getCount();
  311. if (town && slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
  312. continue;
  313. if(currentStackValue.marketValue() < leastValuableStackValue.marketValue())
  314. {
  315. leastValuableStackValue = currentStackValue;
  316. leastValuableSlot = slot.first;
  317. }
  318. }
  319. }
  320. // Decide whether to replace the least valuable stack
  321. if(newStackValue.marketValue() <= leastValuableStackValue.marketValue())
  322. {
  323. continue; // Skip if the new stack isn't worth replacing
  324. }
  325. else
  326. {
  327. shouldDisband = true;
  328. }
  329. }
  330. else
  331. {
  332. freeHeroSlots--; //new slot will be occupied
  333. }
  334. }
  335. vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
  336. int disbandMalus = 0;
  337. if (shouldDisband)
  338. {
  339. disbandMalus = leastValuableStackValue / ci.creID.toCreature()->getFullRecruitCost();
  340. alreadyDisbanded.insert(leastValuableSlot);
  341. }
  342. ci.count -= disbandMalus;
  343. if(ci.count <= 0)
  344. continue;
  345. ci.level = i; //this is important for Dungeon Summoning Portal
  346. creaturesInDwellings.push_back(ci);
  347. availableRes -= ci.creID.toCreature()->getFullRecruitCost() * ci.count;
  348. }
  349. return creaturesInDwellings;
  350. }
  351. ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source, const TerrainId & armyTerrain) const
  352. {
  353. if(source->stacksCount() == 0)
  354. {
  355. return 0;
  356. }
  357. auto bestArmy = getBestArmy(armyCarrier, target, source, armyTerrain);
  358. uint64_t newArmy = 0;
  359. uint64_t oldArmy = target->getArmyStrength();
  360. for(auto & slot : bestArmy)
  361. {
  362. newArmy += slot.power;
  363. }
  364. return newArmy > oldArmy ? newArmy - oldArmy : 0;
  365. }
  366. uint64_t ArmyManager::evaluateStackPower(const Creature * creature, int count) const
  367. {
  368. return creature->getAIValue() * count;
  369. }
  370. SlotInfo ArmyManager::getTotalCreaturesAvailable(CreatureID creatureID) const
  371. {
  372. auto creatureInfo = totalArmy.find(creatureID);
  373. return creatureInfo == totalArmy.end() ? SlotInfo() : creatureInfo->second;
  374. }
  375. void ArmyManager::update()
  376. {
  377. logAi->trace("Start analysing army");
  378. std::vector<const CCreatureSet *> total;
  379. auto heroes = cb->getHeroesInfo();
  380. auto towns = cb->getTownsInfo();
  381. std::copy(heroes.begin(), heroes.end(), std::back_inserter(total));
  382. std::copy(towns.begin(), towns.end(), std::back_inserter(total));
  383. totalArmy.clear();
  384. for(auto army : total)
  385. {
  386. for(auto slot : army->Slots())
  387. {
  388. totalArmy[slot.second->getCreatureID()].count += slot.second->count;
  389. }
  390. }
  391. for(auto & army : totalArmy)
  392. {
  393. army.second.creature = army.first.toCreature();
  394. army.second.power = evaluateStackPower(army.second.creature, army.second.count);
  395. }
  396. }
  397. std::vector<SlotInfo> ArmyManager::convertToSlots(const CCreatureSet * army) const
  398. {
  399. std::vector<SlotInfo> result;
  400. for(auto slot : army->Slots())
  401. {
  402. SlotInfo slotInfo;
  403. slotInfo.creature = slot.second->getCreatureID().toCreature();
  404. slotInfo.count = slot.second->count;
  405. slotInfo.power = evaluateStackPower(slotInfo.creature, slotInfo.count);
  406. result.push_back(slotInfo);
  407. }
  408. return result;
  409. }
  410. std::vector<StackUpgradeInfo> ArmyManager::getHillFortUpgrades(const CCreatureSet * army) const
  411. {
  412. std::vector<StackUpgradeInfo> upgrades;
  413. for(auto creature : army->Slots())
  414. {
  415. CreatureID initial = creature.second->getCreatureID();
  416. auto possibleUpgrades = initial.toCreature()->upgrades;
  417. if(possibleUpgrades.empty())
  418. continue;
  419. CreatureID strongestUpgrade = *vstd::minElementByFun(possibleUpgrades, [](CreatureID cre) -> uint64_t
  420. {
  421. return cre.toCreature()->getAIValue();
  422. });
  423. StackUpgradeInfo upgrade = StackUpgradeInfo(initial, strongestUpgrade, creature.second->count);
  424. if(initial.toCreature()->getLevel() == 1)
  425. upgrade.cost = TResources();
  426. upgrades.push_back(upgrade);
  427. }
  428. return upgrades;
  429. }
  430. std::vector<StackUpgradeInfo> ArmyManager::getDwellingUpgrades(const CCreatureSet * army, const CGDwelling * dwelling) const
  431. {
  432. std::vector<StackUpgradeInfo> upgrades;
  433. for(auto creature : army->Slots())
  434. {
  435. CreatureID initial = creature.second->getCreatureID();
  436. auto possibleUpgrades = initial.toCreature()->upgrades;
  437. vstd::erase_if(possibleUpgrades, [&](CreatureID creID) -> bool
  438. {
  439. for(auto pair : dwelling->creatures)
  440. {
  441. if(vstd::contains(pair.second, creID))
  442. return false;
  443. }
  444. return true;
  445. });
  446. if(possibleUpgrades.empty())
  447. continue;
  448. CreatureID strongestUpgrade = *vstd::minElementByFun(possibleUpgrades, [](CreatureID cre) -> uint64_t
  449. {
  450. return cre.toCreature()->getAIValue();
  451. });
  452. StackUpgradeInfo upgrade = StackUpgradeInfo(initial, strongestUpgrade, creature.second->count);
  453. upgrades.push_back(upgrade);
  454. }
  455. return upgrades;
  456. }
  457. std::vector<StackUpgradeInfo> ArmyManager::getPossibleUpgrades(const CCreatureSet * army, const CGObjectInstance * upgrader) const
  458. {
  459. std::vector<StackUpgradeInfo> upgrades;
  460. if(upgrader->ID == Obj::HILL_FORT)
  461. {
  462. upgrades = getHillFortUpgrades(army);
  463. }
  464. else
  465. {
  466. auto dwelling = dynamic_cast<const CGDwelling *>(upgrader);
  467. if(dwelling)
  468. {
  469. upgrades = getDwellingUpgrades(army, dwelling);
  470. }
  471. }
  472. return upgrades;
  473. }
  474. ArmyUpgradeInfo ArmyManager::calculateCreaturesUpgrade(
  475. const CCreatureSet * army,
  476. const CGObjectInstance * upgrader,
  477. const TResources & availableResources) const
  478. {
  479. if(!upgrader)
  480. return ArmyUpgradeInfo();
  481. std::vector<StackUpgradeInfo> upgrades = getPossibleUpgrades(army, upgrader);
  482. vstd::erase_if(upgrades, [&](const StackUpgradeInfo & u) -> bool
  483. {
  484. return !availableResources.canAfford(u.cost);
  485. });
  486. if(upgrades.empty())
  487. return ArmyUpgradeInfo();
  488. std::sort(upgrades.begin(), upgrades.end(), [](const StackUpgradeInfo & u1, const StackUpgradeInfo & u2) -> bool
  489. {
  490. return u1.upgradeValue > u2.upgradeValue;
  491. });
  492. TResources resourcesLeft = availableResources;
  493. ArmyUpgradeInfo result;
  494. result.resultingArmy = convertToSlots(army);
  495. for(auto upgrade : upgrades)
  496. {
  497. if(resourcesLeft.canAfford(upgrade.cost))
  498. {
  499. SlotInfo upgradedArmy;
  500. upgradedArmy.creature = upgrade.upgradedCreature.toCreature();
  501. upgradedArmy.count = upgrade.count;
  502. upgradedArmy.power = evaluateStackPower(upgradedArmy.creature, upgradedArmy.count);
  503. auto slotToReplace = std::find_if(result.resultingArmy.begin(), result.resultingArmy.end(), [&](const SlotInfo & slot) -> bool {
  504. return slot.count == upgradedArmy.count && slot.creature->getId() == upgrade.initialCreature;
  505. });
  506. resourcesLeft -= upgrade.cost;
  507. result.upgradeCost += upgrade.cost;
  508. result.upgradeValue += upgrade.upgradeValue;
  509. *slotToReplace = upgradedArmy;
  510. }
  511. }
  512. return result;
  513. }
  514. }