From 8cae3398ba93d8c7de0879710c529f276d9148b9 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Sun, 28 Nov 2021 15:57:38 +0300 Subject: [PATCH] Feature: Army Management Shortcuts should work as in HD+ Mod --- CCallback.cpp | 29 ++++ CCallback.h | 14 +- README.md | 2 +- client/NetPacksClient.cpp | 24 +++ client/widgets/CGarrisonInt.cpp | 241 +++++++++++++++++++++----- client/widgets/CGarrisonInt.h | 18 +- lib/CCreatureSet.cpp | 139 ++++++++++++++- lib/CCreatureSet.h | 26 ++- lib/NetPacks.h | 128 +++++++++++++- lib/NetPacksLib.cpp | 15 ++ lib/registerTypes/RegisterTypes.h | 6 + server/CGameHandler.cpp | 270 +++++++++++++++++++++++++++++- server/CGameHandler.h | 8 +- server/CQuery.cpp | 32 +++- server/NetPacksServer.cpp | 20 +++ 15 files changed, 916 insertions(+), 56 deletions(-) diff --git a/CCallback.cpp b/CCallback.cpp index 81b3e9274..7b50c3079 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -109,6 +109,7 @@ int CCallback::mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, S 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); @@ -116,6 +117,34 @@ int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, Sl return 0; } +int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) +{ + BulkMoveArmy pack(srcArmy, destArmy, srcSlot); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany) +{ + BulkSplitStack pack(armyId, srcSlot, howMany); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) +{ + BulkSmartSplitStack pack(armyId, srcSlot); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) +{ + BulkMergeStacks pack(armyId, srcSlot); + sendRequest(&pack); + return 0; +} + bool CCallback::dismissHero(const CGHeroInstance *hero) { if(player!=hero->tempOwner) return false; diff --git a/CCallback.h b/CCallback.h index ef83589ed..76f4413d0 100644 --- a/CCallback.h +++ b/CCallback.h @@ -78,6 +78,12 @@ public: 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; + + // To implement high-level army management bulk actions + virtual int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) = 0; + virtual int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) = 0; + virtual int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) = 0; + virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0; }; struct CPackForServer; @@ -99,7 +105,9 @@ public: friend class CClient; }; -class CCallback : public CPlayerSpecificInfoCallback, public IGameActionCallback, public CBattleCallback +class CCallback : public CPlayerSpecificInfoCallback, + public IGameActionCallback, + public CBattleCallback { public: CCallback(CGameState * GS, boost::optional Player, CClient *C); @@ -125,6 +133,10 @@ public: int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) override; + int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) override; + int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) override; + int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) override; + int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) override; bool dismissHero(const CGHeroInstance * hero) override; bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override; bool assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; diff --git a/README.md b/README.md index 2e25e4934..76a9ea11a 100644 --- a/README.md +++ b/README.md @@ -35,4 +35,4 @@ Platform support is constantly tested by continuous integration and CMake config 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-2020 VCMI Team (check AUTHORS file for the contributors list) +Copyright (C) 2007-2022 VCMI Team (check AUTHORS file for the contributors list) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 266e32db8..f85b8a07b 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -238,6 +238,30 @@ void RebalanceStacks::applyCl(CClient * cl) dispatchGarrisonChange(cl, srcArmy, dstArmy); } +void BulkRebalanceStacks::applyCl(CClient * cl) +{ + if(!moves.empty()) + { + auto destArmy = moves[0].srcArmy == moves[0].dstArmy + ? ObjectInstanceID() + : moves[0].dstArmy; + dispatchGarrisonChange(cl, moves[0].srcArmy, destArmy); + } +} + +void BulkSmartRebalanceStacks::applyCl(CClient * cl) +{ + if(!moves.empty()) + { + assert(moves[0].srcArmy == moves[0].dstArmy); + dispatchGarrisonChange(cl, moves[0].srcArmy, ObjectInstanceID()); + } + else if(!changes.empty()) + { + dispatchGarrisonChange(cl, changes[0].army, ObjectInstanceID()); + } +} + void PutArtifact::applyCl(CClient *cl) { callInterfaceIfPresent(cl, al.owningPlayer(), &IGameEventsReceiver::artifactPut, al); diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index ba3a08e14..22e3ce96a 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -74,18 +74,23 @@ void CGarrisonSlot::hover (bool on) } else { - if(upg == EGarrisonType::UP) + const bool isHeroOnMap = owner->armedObjs[0] // Hero is not a visitor and not a garrison defender + && owner->armedObjs[0]->ID == Obj::HERO + && (!owner->armedObjs[1] || owner->armedObjs[1]->ID == Obj::HERO) // one hero or we are in the Heroes exchange window + && !(static_cast(owner->armedObjs[0]))->inTownGarrison; + + if(isHeroOnMap) + { + temp = CGI->generaltexth->allTexts[481]; //Select %s + } + else if(upg == EGarrisonType::UP) { temp = CGI->generaltexth->tcommands[12]; //Select %s (in garrison) } - else if(owner->armedObjs[0] && (owner->armedObjs[0]->ID == Obj::TOWN || owner->armedObjs[0]->ID == Obj::HERO)) + else // Hero is visiting some object (town, mine, etc) { temp = CGI->generaltexth->tcommands[32]; //Select %s (visiting) } - else - { - temp = CGI->generaltexth->allTexts[481]; //Select %s - } boost::algorithm::replace_first(temp,"%s",creature->nameSing); } } @@ -140,6 +145,18 @@ bool CGarrisonSlot::ally() const return PlayerRelations::ALLIES == LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, getObj()->tempOwner); } +std::function CGarrisonSlot::getDismiss() const +{ + const bool canDismiss = getObj()->tempOwner == LOCPLINT->playerID + && (getObj()->stacksCount() > 1 || + !getObj()->needsLastStack()); + + return canDismiss ? [=]() + { + LOCPLINT->cb->dismissCreature(getObj(), ID); + } : (std::function)nullptr; +} + /// The creature slot has been clicked twice, therefore the creature info should be shown /// @return Whether the view should be refreshed bool CGarrisonSlot::viewInfo() @@ -148,11 +165,9 @@ bool CGarrisonSlot::viewInfo() LOCPLINT->cb->getUpgradeInfo(getObj(), ID, pom); bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.oldID>=0; //upgrade is possible - bool canDismiss = getObj()->tempOwner == LOCPLINT->playerID && (getObj()->stacksCount()>1 || !getObj()->needsLastStack()); std::function upgr = nullptr; - std::function dism = nullptr; + auto dism = getDismiss(); if(canUpgrade) upgr = [=] (CreatureID newID) { LOCPLINT->cb->upgradeCreature(getObj(), ID, newID); }; - if(canDismiss) dism = [=](){ LOCPLINT->cb->dismissCreature(getObj(), ID); }; owner->selectSlot(nullptr); owner->setSplittingMode(false); @@ -288,13 +303,17 @@ void CGarrisonSlot::clickLeft(tribool down, bool previousState) { bool refr = false; const CGarrisonSlot * selection = owner->getSelection(); + if(!selection) { - refr = highlightOrDropArtifact(); + refr = highlightOrDropArtifact(); // Affects selection handleSplittingShortcuts(); } else if(selection == this) - refr = viewInfo(); + { + if(!handleSplittingShortcuts()) + refr = viewInfo(); // Affects selection + } // Re-highlight if troops aren't removable or not ours. else if (mustForceReselection()) { @@ -403,34 +422,67 @@ CGarrisonSlot::CGarrisonSlot(CGarrisonInt * Owner, int x, int y, SlotID IID, CGa update(); } -void CGarrisonSlot::splitIntoParts(CGarrisonSlot::EGarrisonType type, int amount, int maxOfSplittedSlots) +void CGarrisonSlot::splitIntoParts(CGarrisonSlot::EGarrisonType type, int amount) { + auto empty = owner->getEmptySlot(type); + + if(empty == SlotID()) + return; + owner->pb = type; - for(CGarrisonSlot * slot : owner->getEmptySlots(type)) - { - owner->p2 = slot->ID; - owner->splitStacks(1, amount); - maxOfSplittedSlots--; - if(!maxOfSplittedSlots || owner->getSelection()->myStack->count <= 1) - break; - } + owner->p2 = empty; + owner->splitStacks(1, amount); } -void CGarrisonSlot::handleSplittingShortcuts() +bool CGarrisonSlot::handleSplittingShortcuts() { const Uint8 * state = SDL_GetKeyboardState(NULL); - if(owner->getSelection() && owner->getEmptySlots(owner->getSelection()->upg).size() && owner->getSelection()->myStack->count > 1) + const bool isAlt = !!state[SDL_SCANCODE_LALT]; + const bool isLShift = !!state[SDL_SCANCODE_LSHIFT]; + const bool isLCtrl = !!state[SDL_SCANCODE_LCTRL]; + + if(!isAlt && !isLShift && !isLCtrl) + return false; // This is only case when return false + + auto selected = owner->getSelection(); + if(!selected) + return true; // Some Shortcusts are pressed but there are no appropriate actions + + auto units = selected->myStack->count; + if(units < 1) + return true; + + if (isLShift && isLCtrl && isAlt) { - if(state[SDL_SCANCODE_LCTRL] && state[SDL_SCANCODE_LSHIFT]) - splitIntoParts(owner->getSelection()->upg, 1, 7); - else if(state[SDL_SCANCODE_LCTRL]) - splitIntoParts(owner->getSelection()->upg, 1, 1); - else if(state[SDL_SCANCODE_LSHIFT]) - splitIntoParts(owner->getSelection()->upg, owner->getSelection()->myStack->count/2 , 1); - else - return; - owner->selectSlot(nullptr); + owner->bulkMoveArmy(selected); } + else if(isLCtrl && isAlt) + { + owner->moveStackToAnotherArmy(selected); + } + else if(isLShift && isAlt) + { + auto dismiss = getDismiss(); + if(dismiss) + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[12], dismiss, nullptr); + } + else if(isAlt) + { + owner->bulkMergeStacks(selected); + } + else + { + if(units <= 1) + return true; + + if(isLCtrl && isLShift) + owner->bulkSplitStack(selected); + else if(isLShift) + owner->bulkSmartSplitStack(selected); + else + splitIntoParts(selected->upg, 1); // LCtrl + } + return true; } void CGarrisonInt::addSplitBtn(std::shared_ptr button) @@ -492,6 +544,115 @@ void CGarrisonInt::splitStacks(int, int amountRight) LOCPLINT->cb->splitStack(armedObjs[getSelection()->upg], armedObjs[pb], getSelection()->ID, p2, amountRight); } +bool CGarrisonInt::checkSelected(const CGarrisonSlot * selected, TQuantity min) const +{ + return selected && selected->myStack && selected->myStack->count > min && selected->creature; +} + +void CGarrisonInt::moveStackToAnotherArmy(const CGarrisonSlot * selected) +{ + if(!checkSelected(selected)) + return; + + const auto srcArmyType = selected->upg; + const auto destArmyType = srcArmyType == CGarrisonSlot::UP + ? CGarrisonSlot::DOWN + : CGarrisonSlot::UP; + + auto srcArmy = armedObjs[srcArmyType]; + auto destArmy = armedObjs[destArmyType]; + + if(!destArmy) + return; + + auto destSlot = destArmy->getSlotFor(selected->creature); + + if(destSlot == SlotID()) + return; + + const auto srcSlot = selected->ID; + const bool isDestSlotEmpty = !destArmy->getStackCount(destSlot); + + if(isDestSlotEmpty && !destArmy->getStackCount(srcSlot)) + destSlot = srcSlot; // Same place is more preferable + + const bool isLastStack = srcArmy->stacksCount() == 1 && srcArmy->needsLastStack(); + auto srcAmount = selected->myStack->count - (isLastStack ? 1 : 0); + + if(!srcAmount) + return; + + if(!isDestSlotEmpty || isLastStack) + { + srcAmount += destArmy->getStackCount(destSlot); // Due to 'split' implementation in the 'CGameHandler::arrangeStacks' + LOCPLINT->cb->splitStack(srcArmy, destArmy, srcSlot, destSlot, srcAmount); + } + else + { + LOCPLINT->cb->swapCreatures(srcArmy, destArmy, srcSlot, destSlot); + } +} + +void CGarrisonInt::bulkMoveArmy(const CGarrisonSlot * selected) +{ + if(!checkSelected(selected)) + return; + + const auto srcArmyType = selected->upg; + const auto destArmyType = (srcArmyType == CGarrisonSlot::UP) + ? CGarrisonSlot::DOWN + : CGarrisonSlot::UP; + + auto srcArmy = armedObjs[srcArmyType]; + auto destArmy = armedObjs[destArmyType]; + + if(!destArmy) + return; + + const auto srcSlot = selected->ID; + LOCPLINT->cb->bulkMoveArmy(srcArmy->id, destArmy->id, srcSlot); +} + +void CGarrisonInt::bulkMergeStacks(const CGarrisonSlot * selected) +{ + if(!checkSelected(selected)) + return; + + const auto type = selected->upg; + + if(!armedObjs[type]->hasCreatureSlots(selected->creature, selected->ID)) + return; + + LOCPLINT->cb->bulkMergeStacks(armedObjs[type]->id, selected->ID); +} + +void CGarrisonInt::bulkSplitStack(const CGarrisonSlot * selected) +{ + if(!checkSelected(selected, 1)) // check if > 1 + return; + + const auto type = selected->upg; + + if(!hasEmptySlot(type)) + return; + + LOCPLINT->cb->bulkSplitStack(armedObjs[type]->id, selected->ID); +} + +void CGarrisonInt::bulkSmartSplitStack(const CGarrisonSlot * selected) +{ + if(!checkSelected(selected, 1)) + return; + + const auto type = selected->upg; + + // Do not disturb the server if the creature is already balanced + if(!hasEmptySlot(type) && armedObjs[type]->isCreatureBalanced(selected->creature)) + return; + + LOCPLINT->cb->bulkSmartSplitStack(armedObjs[type]->id, selected->ID); +} + CGarrisonInt::CGarrisonInt(int x, int y, int inx, const Point & garsOffset, const CArmedInstance * s1, const CArmedInstance * s2, bool _removableUnits, bool smallImgs, bool _twoRows) @@ -513,7 +674,7 @@ CGarrisonInt::CGarrisonInt(int x, int y, int inx, const Point & garsOffset, createSlots(); } -const CGarrisonSlot * CGarrisonInt::getSelection() +const CGarrisonSlot * CGarrisonInt::getSelection() const { return highlighted; } @@ -554,15 +715,15 @@ bool CGarrisonInt::getSplittingMode() return inSplittingMode; } -std::vector CGarrisonInt::getEmptySlots(CGarrisonSlot::EGarrisonType type) +SlotID CGarrisonInt::getEmptySlot(CGarrisonSlot::EGarrisonType type) const { - std::vector emptySlots; - for(auto slot : availableSlots) - { - if(type == slot->upg && ((slot->our() || slot->ally()) && slot->creature == nullptr)) - emptySlots.push_back(slot.get()); - } - return emptySlots; + assert(armedObjs[type]); + return armedObjs[type] ? armedObjs[type]->getFreeSlot() : SlotID(); +} + +bool CGarrisonInt::hasEmptySlot(CGarrisonSlot::EGarrisonType type) const +{ + return getEmptySlot(type) != SlotID(); } void CGarrisonInt::setArmy(const CArmedInstance * army, bool bottomGarrison) diff --git a/client/widgets/CGarrisonInt.h b/client/widgets/CGarrisonInt.h index 9e7b7eda4..afe3dfcdb 100644 --- a/client/widgets/CGarrisonInt.h +++ b/client/widgets/CGarrisonInt.h @@ -45,6 +45,8 @@ class CGarrisonSlot : public CIntObject bool mustForceReselection() const; void setHighlight(bool on); + std::function getDismiss() const; + public: virtual void hover (bool on) override; //call-in const CArmedInstance * getObj() const; @@ -55,8 +57,8 @@ public: void update(); CGarrisonSlot(CGarrisonInt *Owner, int x, int y, SlotID IID, EGarrisonType Upg=EGarrisonType::UP, const CStackInstance * creature_ = nullptr); - void splitIntoParts(EGarrisonType type, int amount, int maxOfSplittedSlots); - void handleSplittingShortcuts(); + void splitIntoParts(EGarrisonType type, int amount); + bool handleSplittingShortcuts(); /// Returns true when some shortcut is pressed, false otherwise friend class CGarrisonInt; }; @@ -70,6 +72,8 @@ class CGarrisonInt :public CIntObject std::vector> availableSlots; ///< Slots of upper and lower garrison void createSlots(); + bool checkSelected(const CGarrisonSlot * selected, TQuantity min = 0) const; + public: int interx; ///< Space between slots Point garOffset; ///< Offset between garrisons (not used if only one hero) @@ -83,12 +87,13 @@ public: owned[2]; ///< player Owns up or down army ([0] upper, [1] lower) void selectSlot(CGarrisonSlot * slot); ///< @param slot null = deselect - const CGarrisonSlot * getSelection(); + const CGarrisonSlot * getSelection() const; void setSplittingMode(bool on); bool getSplittingMode(); - std::vector getEmptySlots(CGarrisonSlot::EGarrisonType type); + bool hasEmptySlot(CGarrisonSlot::EGarrisonType type) const; + SlotID getEmptySlot(CGarrisonSlot::EGarrisonType type) const; const CArmedInstance * armedObjs[2]; ///< [0] is upper, [1] is down @@ -99,6 +104,11 @@ public: void splitClick(); ///< handles click on split button void splitStacks(int amountLeft, int amountRight); ///< TODO: comment me + void moveStackToAnotherArmy(const CGarrisonSlot * selected); + void bulkMoveArmy(const CGarrisonSlot * selected); + void bulkMergeStacks(const CGarrisonSlot * selected); // Gather all creatures of selected type to the selected slot from other hero/garrison slots + void bulkSplitStack(const CGarrisonSlot * selected); // Used to separate one-creature troops from main stack + void bulkSmartSplitStack(const CGarrisonSlot * selected); /// Constructor /// @param x, y Position diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 4c9d23d36..35e6a4a24 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -23,6 +23,12 @@ #include "serializer/JsonSerializeFormat.h" #include "NetPacksBase.h" + +bool CreatureSlotComparer::operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs) +{ + return lhs.first->getAIValue() < rhs.first->getAIValue(); // Descendant order sorting +} + const CStackInstance &CCreatureSet::operator[](SlotID slot) const { auto i = stacks.find(slot); @@ -72,7 +78,7 @@ SlotID CCreatureSet::getSlotFor(CreatureID creature, ui32 slotsAmount) const /*r SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const { - assert(c->valid()); + assert(c && c->valid()); for(auto & elem : stacks) { assert(elem.second->type->valid()); @@ -84,6 +90,75 @@ SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const return getFreeSlot(slotsAmount); } +bool CCreatureSet::hasCreatureSlots(const CCreature * c, SlotID exclude) const +{ + assert(c && c->valid()); + for(auto & elem : stacks) // elem is const + { + if(elem.first == exclude) // Check slot + continue; + + if(!elem.second || !elem.second->type) // Check creature + continue; + + assert(elem.second->type->valid()); + + if(elem.second->type == c) + return true; + } + return false; +} + +std::vector CCreatureSet::getCreatureSlots(const CCreature * c, SlotID exclude, TQuantity ignoreAmount) const +{ + assert(c && c->valid()); + std::vector result; + + for(auto & elem : stacks) + { + if(elem.first == exclude) + continue; + + if(!elem.second || !elem.second->type || elem.second->type != c) + continue; + + if(elem.second->count == ignoreAmount || elem.second->count < 1) + continue; + + assert(elem.second->type->valid()); + result.push_back(elem.first); + } + return result; +} + +bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmount) const +{ + assert(c && c->valid()); + TQuantity max = 0; + TQuantity min = std::numeric_limits::max(); + + for(auto & elem : stacks) + { + if(!elem.second || !elem.second->type || elem.second->type != c) + continue; + + const auto count = elem.second->count; + + if(count == ignoreAmount || count < 1) + continue; + + assert(elem.second->type->valid()); + + if(count > max) + max = count; + if(count < min) + min = count; + if(max - min > 1) + return false; + } + return true; +} + SlotID CCreatureSet::getFreeSlot(ui32 slotsAmount) const { for(ui32 i=0; i CCreatureSet::getFreeSlots(ui32 slotsAmount) const +{ + std::vector freeSlots; + + for(ui32 i = 0; i < slotsAmount; i++) + { + auto slot = SlotID(i); + + if(!vstd::contains(stacks, slot)) + freeSlots.push_back(slot); + } + return freeSlots; +} + +std::queue CCreatureSet::getFreeSlotsQueue(ui32 slotsAmount) const +{ + std::queue freeSlots; + + for (ui32 i = 0; i < slotsAmount; i++) + { + auto slot = SlotID(i); + + if(!vstd::contains(stacks, slot)) + freeSlots.push(slot); + } + return freeSlots; +} + +TMapCreatureSlot CCreatureSet::getCreatureMap() const +{ + TMapCreatureSlot creatureMap; + TMapCreatureSlot::key_compare keyComp = creatureMap.key_comp(); + + // https://stackoverflow.com/questions/97050/stdmap-insert-or-stdmap-find + // https://www.cplusplus.com/reference/map/map/key_comp/ + for(auto pair : stacks) + { + auto creature = pair.second->type; + auto slot = pair.first; + TMapCreatureSlot::iterator lb = creatureMap.lower_bound(creature); + + if(lb != creatureMap.end() && !(keyComp(creature, lb->first))) + continue; + + creatureMap.insert(lb, TMapCreatureSlot::value_type(creature, slot)); + } + return creatureMap; +} + +TCreatureQueue CCreatureSet::getCreatureQueue(SlotID exclude) const +{ + TCreatureQueue creatureQueue; + + for(auto pair : stacks) + { + if(pair.first == exclude) + continue; + creatureQueue.push(std::make_pair(pair.second->type, pair.first)); + } + return creatureQueue; +} + TQuantity CCreatureSet::getStackCount(SlotID slot) const { auto i = stacks.find(slot); diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index e0a1d2a58..f073f5671 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -142,6 +142,20 @@ public: typedef std::map TSlots; typedef std::map> TSimpleSlots; +typedef std::pair TPairCreatureSlot; +typedef std::map TMapCreatureSlot; + +struct DLL_LINKAGE CreatureSlotComparer +{ + bool operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs); +}; + +typedef std::priority_queue< + TPairCreatureSlot, + std::vector, + CreatureSlotComparer +> TCreatureQueue; + class IArmyDescriptor { public: @@ -209,7 +223,17 @@ public: SlotID findStack(const CStackInstance *stack) const; //-1 if none SlotID getSlotFor(CreatureID creature, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available SlotID getSlotFor(const CCreature *c, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available - SlotID getFreeSlot(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; + bool hasCreatureSlots(const CCreature * c, SlotID exclude) const; + std::vector getCreatureSlots(const CCreature * c, SlotID exclude, TQuantity ignoreAmount = -1) const; + bool isCreatureBalanced(const CCreature* c, TQuantity ignoreAmount = 1) const; // Check if the creature is evenly distributed across slots + + SlotID getFreeSlot(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns first free slot + std::vector getFreeSlots(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; + std::queue getFreeSlotsQueue(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; + + TMapCreatureSlot getCreatureMap() const; + TCreatureQueue getCreatureQueue(SlotID exclude) const; + bool mergableStacks(std::pair &out, SlotID preferable = SlotID()) const; //looks for two same stacks, returns slot positions; bool validTypes(bool allowUnrandomized = false) const; //checks if all types of creatures are set properly bool slotEmpty(SlotID slot) const; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index c7bd85c0d..3016b2d19 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -886,7 +886,7 @@ struct RebalanceStacks : CGarrisonOperationPack TQuantity count; void applyCl(CClient *cl); - DLL_LINKAGE void applyGs(CGameState *gs); + DLL_LINKAGE void applyGs(CGameState * gs); template void serialize(Handler &h, const int version) { @@ -898,6 +898,36 @@ struct RebalanceStacks : CGarrisonOperationPack } }; +struct BulkRebalanceStacks : CGarrisonOperationPack +{ + std::vector moves; + + void applyCl(CClient * cl); + DLL_LINKAGE void applyGs(CGameState * gs); + + template + void serialize(Handler & h, const int version) + { + h & moves; + } +}; + +struct BulkSmartRebalanceStacks : CGarrisonOperationPack +{ + std::vector moves; + std::vector changes; + + void applyCl(CClient * cl); + DLL_LINKAGE void applyGs(CGameState * gs); + + template + void serialize(Handler & h, const int version) + { + h & moves; + h & changes; + } +}; + struct GetEngagedHeroIds : boost::static_visitor> { boost::optional operator()(const ConstTransitivePtr &h) const @@ -1946,6 +1976,102 @@ struct ArrangeStacks : public CPackForServer } }; +struct BulkMoveArmy : public CPackForServer +{ + SlotID srcSlot; + ObjectInstanceID srcArmy; + ObjectInstanceID destArmy; + + BulkMoveArmy() + {}; + + BulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) + : srcArmy(srcArmy), destArmy(destArmy), srcSlot(srcSlot) + {}; + + bool applyGh(CGameHandler * gh); + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & srcSlot; + h & srcArmy; + h & destArmy; + } +}; + +struct BulkSplitStack : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + si32 amount; + + BulkSplitStack() : amount(0) + {}; + + BulkSplitStack(ObjectInstanceID srcOwner, SlotID src, si32 howMany) + : src(src), srcOwner(srcOwner), amount(howMany) + {}; + + bool applyGh(CGameHandler * gh); + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + h & amount; + } +}; + +struct BulkMergeStacks : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + + BulkMergeStacks() + {}; + + BulkMergeStacks(ObjectInstanceID srcOwner, SlotID src) + : src(src), srcOwner(srcOwner) + {}; + + bool applyGh(CGameHandler * gh); + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + } +}; + +struct BulkSmartSplitStack : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + + BulkSmartSplitStack() + {}; + + BulkSmartSplitStack(ObjectInstanceID srcOwner, SlotID src) + : src(src), srcOwner(srcOwner) + {}; + + bool applyGh(CGameHandler * gh); + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + } +}; + struct DisbandCreature : public CPackForServer { DisbandCreature(){}; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 90ba23581..48499b089 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1020,6 +1020,21 @@ DLL_LINKAGE void RebalanceStacks::applyGs(CGameState * gs) CBonusSystemNode::treeHasChanged(); } +DLL_LINKAGE void BulkRebalanceStacks::applyGs(CGameState * gs) +{ + for(auto & move : moves) + move.applyGs(gs); +} + +DLL_LINKAGE void BulkSmartRebalanceStacks::applyGs(CGameState * gs) +{ + for(auto & move : moves) + move.applyGs(gs); + + for(auto & change : changes) + change.applyGs(gs); +} + DLL_LINKAGE void PutArtifact::applyGs(CGameState *gs) { assert(art->canBePutAt(al)); diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 9178c38cc..a20235254 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -317,6 +317,8 @@ void registerTypesClientPacks2(Serializer &s) s.template registerType(); s.template registerType(); + s.template registerType(); + s.template registerType(); } template @@ -347,6 +349,10 @@ void registerTypesServerPacks(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); } template diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 87e76831a..093075902 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1629,6 +1629,9 @@ int CGameHandler::moveStack(int stack, BattleHex dest) CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) + , complainNoCreatures("No creatures to split") + , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") + , complainInvalidSlot("Invalid slot accessed!") { QID = 1; IObjectInterface::cb = this; @@ -2962,6 +2965,259 @@ void CGameHandler::load(const std::string & filename) gs->updateOnLoad(lobby->si.get()); } +bool CGameHandler::bulkSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner, si32 howMany) +{ + if(!slotSrc.validSlot() && complain(complainInvalidSlot)) + return false; + + const CArmedInstance * army = static_cast(getObjInstance(srcOwner)); + const CCreatureSet & creatureSet = *army; + + if((!vstd::contains(creatureSet.stacks, slotSrc) && complain(complainNoCreatures)) + || (howMany < 1 && complain("Invalid split parameter!"))) + { + return false; + } + auto actualAmount = army->getStackCount(slotSrc); + + if(actualAmount <= howMany && complain(complainNotEnoughCreatures)) // '<=' because it's not intended just for moving a stack + return false; + + auto freeSlots = creatureSet.getFreeSlots(); + + if(freeSlots.empty() && complain("No empty stacks")) + return false; + + BulkRebalanceStacks bulkRS; + + for(auto slot : freeSlots) + { + RebalanceStacks rs; + rs.srcArmy = army->id; + rs.dstArmy = army->id; + rs.srcSlot = slotSrc; + rs.dstSlot = slot; + rs.count = howMany; + + bulkRS.moves.push_back(rs); + actualAmount -= howMany; + + if(actualAmount <= howMany) + break; + } + sendAndApply(&bulkRS); + return true; +} + +bool CGameHandler::bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner) +{ + if(!slotSrc.validSlot() && complain(complainInvalidSlot)) + return false; + + const CArmedInstance * army = static_cast(getObjInstance(srcOwner)); + const CCreatureSet & creatureSet = *army; + + if(!vstd::contains(creatureSet.stacks, slotSrc) && complain(complainNoCreatures)) + return false; + + auto actualAmount = creatureSet.getStackCount(slotSrc); + + if(actualAmount < 1 && complain(complainNoCreatures)) + return false; + + auto currentCreature = creatureSet.getCreature(slotSrc); + + if(!currentCreature && complain(complainNoCreatures)) + return false; + + auto creatureSlots = creatureSet.getCreatureSlots(currentCreature, slotSrc); + + if(!creatureSlots.size()) + return false; + + BulkRebalanceStacks bulkRS; + + for(auto slot : creatureSlots) + { + RebalanceStacks rs; + rs.srcArmy = army->id; + rs.dstArmy = army->id; + rs.srcSlot = slot; + rs.dstSlot = slotSrc; + rs.count = creatureSet.getStackCount(slot); + bulkRS.moves.push_back(rs); + } + sendAndApply(&bulkRS); + return true; +} + +bool CGameHandler::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) +{ + if(!srcSlot.validSlot() && complain(complainInvalidSlot)) + return false; + + const CArmedInstance * armySrc = static_cast(getObjInstance(srcArmy)); + const CCreatureSet & setSrc = *armySrc; + + if(!vstd::contains(setSrc.stacks, srcSlot) && complain(complainNoCreatures)) + return false; + + const CArmedInstance * armyDest = static_cast(getObjInstance(destArmy)); + const CCreatureSet & setDest = *armyDest; + auto freeSlots = setDest.getFreeSlotsQueue(); + + typedef std::map> TRebalanceMap; + TRebalanceMap moves; + + auto srcQueue = setSrc.getCreatureQueue(srcSlot); // Exclude srcSlot, it should be moved last + auto slotsLeft = setSrc.stacksCount(); + auto destMap = setDest.getCreatureMap(); + TMapCreatureSlot::key_compare keyComp = destMap.key_comp(); + + while(!srcQueue.empty()) + { + auto pair = srcQueue.top(); + srcQueue.pop(); + + auto currCreature = pair.first; + auto currSlot = pair.second; + const auto quantity = setSrc.getStackCount(currSlot); + + TMapCreatureSlot::iterator lb = destMap.lower_bound(currCreature); + const bool alreadyExists = (lb != destMap.end() && !(keyComp(currCreature, lb->first))); + + if(!alreadyExists) + { + if(freeSlots.empty()) + continue; + + auto currFreeSlot = freeSlots.front(); + freeSlots.pop(); + destMap.insert(lb, TMapCreatureSlot::value_type(currCreature, currFreeSlot)); + } + moves.insert(std::make_pair(currSlot, std::make_pair(destMap[currCreature], quantity))); + slotsLeft--; + } + if(slotsLeft == 1) + { + auto lastCreature = setSrc.getCreature(srcSlot); + auto slotToMove = SlotID(); + // Try to find a slot for last creature + if(destMap.find(lastCreature) == destMap.end()) + { + if(!freeSlots.empty()) + slotToMove = freeSlots.front(); + } + else + { + slotToMove = destMap[lastCreature]; + } + + if(slotToMove != SlotID()) + { + const bool needsLastStack = armySrc->needsLastStack(); + const auto quantity = setSrc.getStackCount(srcSlot) - (needsLastStack ? 1 : 0); + moves.insert(std::make_pair(srcSlot, std::make_pair(slotToMove, quantity))); + } + } + BulkRebalanceStacks bulkRS; + + for(auto & move : moves) + { + RebalanceStacks rs; + rs.srcArmy = armySrc->id; + rs.dstArmy = armyDest->id; + rs.srcSlot = move.first; + rs.dstSlot = move.second.first; + rs.count = move.second.second; + bulkRS.moves.push_back(rs); + } + sendAndApply(&bulkRS); + return true; +} + +bool CGameHandler::bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner) +{ + if(!slotSrc.validSlot() && complain(complainInvalidSlot)) + return false; + + const CArmedInstance * army = static_cast(getObjInstance(srcOwner)); + const CCreatureSet & creatureSet = *army; + + if(!vstd::contains(creatureSet.stacks, slotSrc) && complain(complainNoCreatures)) + return false; + + auto actualAmount = creatureSet.getStackCount(slotSrc); + + if(actualAmount <= 1 && complain(complainNoCreatures)) + return false; + + auto freeSlot = creatureSet.getFreeSlot(); + auto currentCreature = creatureSet.getCreature(slotSrc); + + if(freeSlot == SlotID() && creatureSet.isCreatureBalanced(currentCreature)) + return true; + + auto creatureSlots = creatureSet.getCreatureSlots(currentCreature, SlotID(-1), 1); // Ignore slots where's only 1 creature, don't ignore slotSrc + TQuantity totalCreatures = 0; + + for(auto slot : creatureSlots) + totalCreatures += creatureSet.getStackCount(slot); + + if(totalCreatures <= 1 && complain("Total creatures number is invalid")) + return false; + + if(freeSlot != SlotID()) + creatureSlots.push_back(freeSlot); + + if(creatureSlots.empty() && complain("No available slots for smart rebalancing")) + return false; + + const auto totalCreatureSlots = creatureSlots.size(); + const auto rem = totalCreatures % totalCreatureSlots; + const auto quotient = totalCreatures / totalCreatureSlots; + + // totalCreatures == rem * (quotient + 1) + (totalCreatureSlots - rem) * quotient; + // Proof: r(q+1)+(s-r)q = rq+r+qs-rq = r+qs = total, where total/s = q+r/s + + BulkSmartRebalanceStacks bulkSRS; + + if(freeSlot != SlotID()) + { + RebalanceStacks rs; + rs.srcArmy = rs.dstArmy = army->id; + rs.srcSlot = slotSrc; + rs.dstSlot = freeSlot; + rs.count = 1; + bulkSRS.moves.push_back(rs); + } + auto currSlot = 0; + auto check = 0; + + for(auto slot : creatureSlots) + { + ChangeStackCount csc; + + csc.army = army->id; + csc.slot = slot; + csc.count = (currSlot < rem) + ? quotient + 1 + : quotient; + csc.absoluteValue = true; + bulkSRS.changes.push_back(csc); + currSlot++; + check += csc.count; + } + + if(check != totalCreatures) + { + complain((boost::format("Failure: totalCreatures=%d but check=%d") % totalCreatures % check).str()); + return false; + } + sendAndApply(&bulkSRS); + return true; +} + bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player) { const CArmedInstance * s1 = static_cast(getObjInstance(id1)), @@ -2970,7 +3226,7 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 StackLocation sl1(s1, p1), sl2(s2, p2); if (!sl1.slot.validSlot() || !sl2.slot.validSlot()) { - complain("Invalid slot accessed!"); + complain(complainInvalidSlot); return false; } @@ -3051,8 +3307,8 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 } //general conditions checking - if ((!vstd::contains(S1.stacks,p1) && complain("no creatures to split")) - || (val<1 && complain("no creatures to split")) ) + if ((!vstd::contains(S1.stacks,p1) && complain(complainNoCreatures)) + || (val<1 && complain(complainNoCreatures)) ) { return false; } @@ -3087,7 +3343,7 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 { if (s1->getStackCount(p1) < val)//not enough creatures { - complain("Cannot split that stack, not enough creatures!"); + complain(complainNotEnoughCreatures); return false; } @@ -6793,7 +7049,11 @@ bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) auto query = queries.topQuery(player); if (query && query->blocksPack(pack)) { - complain(boost::str(boost::format("Player %s has to answer queries before attempting any further actions. Top query is %s!") % player % query->toString())); + complain(boost::str(boost::format( + "\r\n| Player \"%s\" has to answer queries before attempting any further actions.\r\n| Top Query: \"%s\"\r\n") + % boost::to_upper_copy(player.getStr()) + % query->toString() + )); return true; } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 547f425f7..2e0c061eb 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -249,6 +249,10 @@ public: bool razeStructure(ObjectInstanceID tid, BuildingID bid); bool disbandCreature( ObjectInstanceID id, SlotID pos ); bool arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player); + bool bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot); + bool bulkSplitStack(SlotID src, ObjectInstanceID srcOwner, si32 howMany); + bool bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner); + bool bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner); void save(const std::string &fname); void load(const std::string &fname); @@ -353,7 +357,9 @@ private: void checkVictoryLossConditions(const std::set & playerColors); void checkVictoryLossConditionsForAll(); - + const std::string complainNoCreatures; + const std::string complainNotEnoughCreatures; + const std::string complainInvalidSlot; }; class ExceptionNotAllowedAction : public std::exception diff --git a/server/CQuery.cpp b/server/CQuery.cpp index c6bebb595..4f8270a6f 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -69,7 +69,25 @@ void CQuery::addPlayer(PlayerColor color) std::string CQuery::toString() const { - std::string ret = boost::str(boost::format("A query of type %s and qid=%d affecting players %s") % typeid(*this).name() % queryID % formatContainer(players)); + const auto size = players.size(); + const std::string plural = size > 1 ? "s" : ""; + std::string names; + + for(size_t i = 0; i < size; i++) + { + names += boost::to_upper_copy(players[i].getStr()); + + if(i < size - 2) + names += ", "; + else if(size > 1 && i == size - 2) + names += " and "; + } + std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s") + % typeid(*this).name() + % queryID + % plural + % names + ); return ret; } @@ -327,6 +345,18 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const if(auto stacks = dynamic_ptr_cast(pack)) return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2); + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy); + if(auto arts = dynamic_ptr_cast(pack)) { if(auto id1 = boost::apply_visitor(GetEngagedHeroIds(), arts->src.artHolder)) diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index c48827b32..a214d486f 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -123,6 +123,26 @@ bool ArrangeStacks::applyGh(CGameHandler * gh) return gh->arrangeStacks(id1, id2, what, p1, p2, val, gh->getPlayerAt(c)); } +bool BulkMoveArmy::applyGh(CGameHandler * gh) +{ + return gh->bulkMoveArmy(srcArmy, destArmy, srcSlot); +} + +bool BulkSplitStack::applyGh(CGameHandler * gh) +{ + return gh->bulkSplitStack(src, srcOwner, amount); +} + +bool BulkMergeStacks::applyGh(CGameHandler* gh) +{ + return gh->bulkMergeStacks(src, srcOwner); +} + +bool BulkSmartSplitStack::applyGh(CGameHandler * gh) +{ + return gh->bulkSmartSplitStack(src, srcOwner); +} + bool DisbandCreature::applyGh(CGameHandler * gh) { throwOnWrongOwner(gh, id);