1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-04-07 07:10:04 +02:00

Merge pull request #2868 from IvanSavenko/simultaneous_turns

Simultaneous turns
This commit is contained in:
Ivan Savenko 2023-09-27 15:45:02 +03:00 committed by GitHub
commit 94dbde05a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 442 additions and 120 deletions

View File

@ -41,12 +41,10 @@ bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit)
int CCallback::selectionMade(int selection, QueryID queryID)
{
JsonNode reply(JsonNode::JsonType::DATA_INTEGER);
reply.Integer() = selection;
return sendQueryReply(reply, queryID);
return sendQueryReply(selection, queryID);
}
int CCallback::sendQueryReply(const JsonNode & reply, QueryID queryID)
int CCallback::sendQueryReply(std::optional<int32_t> reply, QueryID queryID)
{
ASSERT_IF_CALLED_WITH_PLAYER
if(queryID == QueryID(-1))

View File

@ -82,7 +82,7 @@ public:
virtual void trade(const IMarket * market, EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr)=0;
virtual int selectionMade(int selection, QueryID queryID) =0;
virtual int sendQueryReply(const JsonNode & reply, QueryID queryID) =0;
virtual int sendQueryReply(std::optional<int32_t> reply, QueryID queryID) =0;
virtual int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//swaps creatures between two possibly different garrisons // TODO: AI-unsafe code - fix it!
virtual int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//joins first stack to the second (creatures must be same type)
virtual int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) =0; //first goes to the second
@ -159,7 +159,7 @@ public:
bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile)
bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where);
int selectionMade(int selection, QueryID queryID) override;
int sendQueryReply(const JsonNode & reply, QueryID queryID) override;
int sendQueryReply(std::optional<int32_t> reply, QueryID queryID) override;
int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override;
int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second
int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second

View File

@ -167,47 +167,61 @@ void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::
CCS->musich->loadTerrainMusicThemes();
initializeHeroTownList();
// always recreate advmap interface to avoid possible memory-corruption bugs
adventureInt.reset(new AdventureMapInterface());
}
void CPlayerInterface::playerEndsTurn(PlayerColor player)
{
EVENT_HANDLER_CALLED_BY_CLIENT;
if (player == playerID)
{
makingTurn = false;
// remove all active dialogs that do not expect query answer
for (;;)
{
auto adventureWindow = GH.windows().topWindow<AdventureMapInterface>();
auto infoWindow = GH.windows().topWindow<CInfoWindow>();
if(adventureWindow != nullptr)
break;
if(infoWindow && infoWindow->ID != QueryID::NONE)
break;
if (infoWindow)
infoWindow->close();
else
GH.windows().popWindows(1);
}
// remove all pending dialogs that do not expect query answer
vstd::erase_if(dialogs, [](const std::shared_ptr<CInfoWindow> & window){
return window->ID == QueryID::NONE;
});
}
}
void CPlayerInterface::playerStartsTurn(PlayerColor player)
{
EVENT_HANDLER_CALLED_BY_CLIENT;
movementController->onPlayerTurnStarted();
if(GH.windows().findWindows<AdventureMapInterface>().empty())
{
// after map load - remove all active windows and replace them with adventure map
// always recreate advmap interface to avoid possible memory-corruption bugs
adventureInt.reset(new AdventureMapInterface());
GH.windows().clear();
GH.windows().pushWindow(adventureInt);
}
// close window from another player
if(auto w = GH.windows().topWindow<CInfoWindow>())
if(w->ID == QueryID::NONE && player != playerID)
w->close();
// remove all dialogs that do not expect query answer
while (!GH.windows().topWindow<AdventureMapInterface>() && !GH.windows().topWindow<CInfoWindow>())
GH.windows().popWindows(1);
EVENT_HANDLER_CALLED_BY_CLIENT;
if (player != playerID && LOCPLINT == this)
{
waitWhileDialog();
bool isHuman = cb->getStartInfo()->playerInfos.count(player) && cb->getStartInfo()->playerInfos.at(player).isControlledByHuman();
adventureInt->onEnemyTurnStarted(player, isHuman);
if (makingTurn == false)
adventureInt->onEnemyTurnStarted(player, isHuman);
}
}
@ -335,6 +349,7 @@ void CPlayerInterface::acceptTurn(QueryID queryID)
}
cb->selectionMade(0, queryID);
movementController->onPlayerTurnStarted();
}
void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose)
@ -1058,15 +1073,12 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
auto selectCallback = [=](int selection)
{
JsonNode reply(JsonNode::JsonType::DATA_INTEGER);
reply.Integer() = selection;
cb->sendQueryReply(reply, askID);
cb->sendQueryReply(selection, askID);
};
auto cancelCallback = [=]()
{
JsonNode reply(JsonNode::JsonType::DATA_NULL);
cb->sendQueryReply(reply, askID);
cb->sendQueryReply(std::nullopt, askID);
};
const std::string localTitle = title.toString();

View File

@ -477,6 +477,13 @@ void CServerHandler::setDifficulty(int to) const
sendLobbyPack(lsd);
}
void CServerHandler::setSimturnsInfo(const SimturnsInfo & info) const
{
LobbySetSimturns pack;
pack.simturnsInfo = info;
sendLobbyPack(pack);
}
void CServerHandler::setTurnTimerInfo(const TurnTimerInfo & info) const
{
LobbySetTurnTime lstt;

View File

@ -66,6 +66,7 @@ public:
virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0;
virtual void setDifficulty(int to) const = 0;
virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0;
virtual void setSimturnsInfo(const SimturnsInfo &) const = 0;
virtual void sendMessage(const std::string & txt) const = 0;
virtual void sendGuiAction(ui8 action) const = 0; // TODO: possibly get rid of it?
virtual void sendStartGame(bool allowOnlyAI = false) const = 0;
@ -148,6 +149,7 @@ public:
void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override;
void setDifficulty(int to) const override;
void setTurnTimerInfo(const TurnTimerInfo &) const override;
void setSimturnsInfo(const SimturnsInfo &) const override;
void sendMessage(const std::string & txt) const override;
void sendGuiAction(ui8 action) const override;
void sendRestartGame() const override;

View File

@ -71,7 +71,7 @@ std::vector<AdventureMapShortcutState> AdventureMapShortcuts::getShortcuts()
{ EShortcut::ADVENTURE_GAME_OPTIONS, optionInMapView(), [this]() { this->adventureOptions(); } },
{ EShortcut::GLOBAL_OPTIONS, optionInMapView(), [this]() { this->systemOptions(); } },
{ EShortcut::ADVENTURE_NEXT_HERO, optionHasNextHero(), [this]() { this->nextHero(); } },
{ EShortcut::GAME_END_TURN, optionInMapView(), [this]() { this->endTurn(); } },
{ EShortcut::GAME_END_TURN, optionCanEndTurn(), [this]() { this->endTurn(); } },
{ EShortcut::ADVENTURE_THIEVES_GUILD, optionInMapView(), [this]() { this->showThievesGuild(); } },
{ EShortcut::ADVENTURE_VIEW_SCENARIO, optionInMapView(), [this]() { this->showScenarioInfo(); } },
{ EShortcut::GAME_SAVE_GAME, optionInMapView(), [this]() { this->saveGame(); } },
@ -453,6 +453,11 @@ bool AdventureMapShortcuts::optionHasNextHero()
return optionInMapView() && nextSuitableHero != nullptr;
}
bool AdventureMapShortcuts::optionCanEndTurn()
{
return optionInMapView() && LOCPLINT->makingTurn;
}
bool AdventureMapShortcuts::optionSpellcasting()
{
return state == EAdventureState::CASTING_SPELL;

View File

@ -78,6 +78,7 @@ public:
bool optionHeroCanMove();
bool optionHasNextHero();
bool optionCanVisitObject();
bool optionCanEndTurn();
bool optionSpellcasting();
bool optionInMapView();
bool optionInWorldView();

View File

@ -59,6 +59,7 @@ public:
std::shared_ptr<CButton> buttonOptions;
std::shared_ptr<CButton> buttonStart;
std::shared_ptr<CButton> buttonBack;
std::shared_ptr<CButton> buttonSimturns;
std::shared_ptr<SelectionTab> tabSel;
std::shared_ptr<OptionsTab> tabOpt;

View File

@ -58,6 +58,12 @@ OptionsTab::OptionsTab() : humanPlayers(0)
CSH->setTurnTimerInfo(tinfo);
}
});
addCallback("setSimturnDuration", [&](int index){
SimturnsInfo info;
info.optionalTurns = index;
CSH->setSimturnsInfo(info);
});
//helper function to parse string containing time to integer reflecting time in seconds
//assumed that input string can be modified by user, function shall support user's intention
@ -214,6 +220,18 @@ void OptionsTab::recreate()
entries.insert(std::make_pair(pInfo.first, std::make_shared<PlayerOptionsEntry>(pInfo.second, * this)));
}
//Simultaneous turns
if(auto turnSlider = widget<CSlider>("labelSimturnsDurationValue"))
turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.optionalTurns);
if(auto w = widget<CLabel>("labelSimturnsDurationValue"))
{
MetaString message;
message.appendRawString("Simturns: up to %d days");
message.replaceNumber(SEL->getStartInfo()->simturnsInfo.optionalTurns);
w->setText(message.toString());
}
const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo;

View File

@ -73,16 +73,41 @@
"adoptHeight": true
},
// timer
{
"name": "simturnsDuration",
"type": "slider",
"orientation": "horizontal",
"position": {"x": 55, "y": 537},
"size": 194,
"callback": "setSimturnDuration",
"itemsVisible": 1,
"itemsTotal": 28,
"selected": 0,
"style": "blue",
"scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32},
"panningStep": 20
},
{
"name": "labelSimturnsDurationValue",
"type": "label",
"font": "small",
"alignment": "center",
"color": "yellow",
"text": "core.genrltxt.521",
"position": {"x": 222, "y": 544}
"color": "white",
"text": "",
"position": {"x": 319, "y": 545}
},
// timer
//{
// "type": "label",
// "font": "small",
// "alignment": "center",
// "color": "yellow",
// "text": "core.genrltxt.521",
// "position": {"x": 222, "y": 544}
//},
{
"name": "labelTurnDurationValue",
"type": "label",
@ -104,7 +129,8 @@
"itemsTotal": 11,
"selected": 11,
"style": "blue",
"scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43},
"scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32},
//"scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43},
"panningStep": 20
},
],

View File

@ -158,6 +158,7 @@ public:
virtual void visitLobbySetCampaignBonus(LobbySetCampaignBonus & pack) {}
virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) {}
virtual void visitLobbySetPlayer(LobbySetPlayer & pack) {}
virtual void visitLobbySetSimturns(LobbySetSimturns & pack) {}
virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) {}
virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) {}
virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) {}

View File

@ -2609,14 +2609,14 @@ struct DLL_LINKAGE BuildBoat : public CPackForServer
struct DLL_LINKAGE QueryReply : public CPackForServer
{
QueryReply() = default;
QueryReply(const QueryID & QID, const JsonNode & Reply)
QueryReply(const QueryID & QID, std::optional<int32_t> Reply)
: qid(QID)
, reply(Reply)
{
}
QueryID qid;
PlayerColor player;
JsonNode reply;
std::optional<int32_t> reply;
virtual void visitTyped(ICPackVisitor & visitor) override;

View File

@ -750,6 +750,11 @@ void LobbySetPlayer::visitTyped(ICPackVisitor & visitor)
visitor.visitLobbySetPlayer(*this);
}
void LobbySetSimturns::visitTyped(ICPackVisitor & visitor)
{
visitor.visitLobbySetSimturns(*this);
}
void LobbySetTurnTime::visitTyped(ICPackVisitor & visitor)
{
visitor.visitLobbySetTurnTime(*this);

View File

@ -250,6 +250,18 @@ struct DLL_LINKAGE LobbySetPlayer : public CLobbyPackToServer
}
};
struct DLL_LINKAGE LobbySetSimturns : public CLobbyPackToServer
{
SimturnsInfo simturnsInfo;
virtual void visitTyped(ICPackVisitor & visitor) override;
template <typename Handler> void serialize(Handler &h, const int version)
{
h & simturnsInfo;
}
};
struct DLL_LINKAGE LobbySetTurnTime : public CLobbyPackToServer
{
TurnTimerInfo turnTimerInfo;

View File

@ -23,6 +23,24 @@ class CMapInfo;
struct PlayerInfo;
class PlayerColor;
struct DLL_LINKAGE SimturnsInfo
{
/// Minimal number of turns that must be played simultaneously even if contact has been detected
int requiredTurns = 0;
/// Maximum number of turns that might be played simultaneously unless contact is detected
int optionalTurns = 0;
/// If set to true, human and 1 AI can act at the same time
bool allowHumanWithAI = true;
template <typename Handler>
void serialize(Handler &h, const int version)
{
h & requiredTurns;
h & optionalTurns;
h & allowHumanWithAI;
}
};
/// Struct which describes the name, the color, the starting bonus of a player
struct DLL_LINKAGE PlayerSettings
{
@ -84,6 +102,7 @@ struct DLL_LINKAGE StartInfo
ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet
ui32 mapfileChecksum; //0 if not relevant
std::string startTimeIso8601;
SimturnsInfo simturnsInfo;
TurnTimerInfo turnTimerInfo;
std::string mapname; // empty for random map, otherwise name of the map or savegame
bool createRandomMap() const { return mapGenOptions != nullptr; }
@ -108,6 +127,7 @@ struct DLL_LINKAGE StartInfo
h & seedPostInit;
h & mapfileChecksum;
h & startTimeIso8601;
h & simturnsInfo;
h & turnTimerInfo;
h & mapname;
h & mapGenOptions;

View File

@ -372,7 +372,7 @@ ObjectInstanceID CGTeleport::getRandomExit(const CGHeroInstance * h) const
bool CGTeleport::isTeleport(const CGObjectInstance * obj)
{
return ((dynamic_cast<const CGTeleport *>(obj)));
return dynamic_cast<const CGTeleport *>(obj) != nullptr;
}
bool CGTeleport::isConnected(const CGTeleport * src, const CGTeleport * dst)

View File

@ -121,6 +121,8 @@ void CPathfinder::calculatePaths()
movement = hlp->getMaxMovePoints(source.node->layer);
if(!hlp->passOneTurnLimitCheck(source))
continue;
if(turn >= hlp->options.turnLimit)
continue;
}
source.isInitialPosition = source.nodeHero == hlp->hero;

View File

@ -19,7 +19,7 @@ class CGWhirlpool;
struct TurnInfo;
struct PathfinderOptions;
class CPathfinder
class DLL_LINKAGE CPathfinder
{
public:
friend class CPathfinderHelper;

View File

@ -30,6 +30,7 @@ PathfinderOptions::PathfinderOptions()
, lightweightFlyingMode(false)
, oneTurnSpecialLayersLimit(true)
, originalMovementRules(false)
, turnLimit(std::numeric_limits<uint8_t>::max())
{
}

View File

@ -68,6 +68,9 @@ struct DLL_LINKAGE PathfinderOptions
/// I find it's reasonable limitation, but it's will make some movements more expensive than in H3.
bool originalMovementRules;
/// Max number of turns to compute. Default = infinite
uint8_t turnLimit;
PathfinderOptions();
};

View File

@ -399,6 +399,7 @@ void registerTypesLobbyPacks(Serializer &s)
s.template registerType<CLobbyPackToServer, LobbySetCampaignBonus>();
s.template registerType<CLobbyPackToServer, LobbySetPlayer>();
s.template registerType<CLobbyPackToServer, LobbySetTurnTime>();
s.template registerType<CLobbyPackToServer, LobbySetSimturns>();
s.template registerType<CLobbyPackToServer, LobbySetDifficulty>();
s.template registerType<CLobbyPackToServer, LobbyForceSetPlayer>();
}

View File

@ -482,11 +482,11 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
if(!parameters.pos.valid() && parameters.caster->getSpellSchoolLevel(owner) >= 2)
{
auto queryCallback = [=](const JsonNode & reply) -> void
auto queryCallback = [=](std::optional<int32_t> reply) -> void
{
if(reply.getType() == JsonNode::JsonType::DATA_INTEGER)
if(reply.has_value())
{
ObjectInstanceID townId(static_cast<si32>(reply.Integer()));
ObjectInstanceID townId(*reply);
const CGObjectInstance * o = env->getCb()->getObj(townId, true);
if(o == nullptr)

View File

@ -56,7 +56,7 @@ public:
virtual bool moveHero(ObjectInstanceID hid, int3 dst, bool teleporting) = 0; //TODO: remove
virtual void genericQuery(Query * request, PlayerColor color, std::function<void(const JsonNode &)> callback) = 0;//TODO: type safety on query, use generic query packet when implemented
virtual void genericQuery(Query * request, PlayerColor color, std::function<void(std::optional<int32_t>)> callback) = 0;//TODO: type safety on query, use generic query packet when implemented
};
namespace spells

View File

@ -1086,8 +1086,18 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
const TerrainTile t = *getTile(hmpos);
const int3 guardPos = gs->guardingCreaturePosition(hmpos);
CGObjectInstance * objectToVisit = nullptr;
CGObjectInstance * guardian = nullptr;
const bool embarking = !h->boat && !t.visitableObjects.empty() && t.visitableObjects.back()->ID == Obj::BOAT;
if (!t.visitableObjects.empty())
objectToVisit = t.visitableObjects.back();
if (isInTheMap(guardPos))
guardian = getTile(guardPos)->visitableObjects.back();
assert(guardian == nullptr || dynamic_cast<CGCreature*>(guardian) != nullptr);
const bool embarking = !h->boat && objectToVisit && objectToVisit->ID == Obj::BOAT;
const bool disembarking = h->boat
&& t.terType->isLand()
&& (dst == h->pos
@ -1110,7 +1120,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
const int cost = pathfinderHelper->getMovementCost(h->visitablePos(), hmpos, nullptr, nullptr, h->movementPointsRemaining());
const bool standAtObstacle = t.blocked && !t.visitable;
const bool standAtWater = !h->boat && t.terType->isWater() && (t.visitableObjects.empty() || !t.visitableObjects.back()->isCoastVisitable());
const bool standAtWater = !h->boat && t.terType->isWater() && (objectToVisit || !objectToVisit->isCoastVisitable());
const auto complainRet = [&](const std::string & message)
{
@ -1120,29 +1130,41 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
return false;
};
if (guardian && getVisitingHero(guardian) != nullptr)
return complainRet("Cannot move hero, destination monster is busy!");
if (objectToVisit && getVisitingHero(objectToVisit) != nullptr)
return complainRet("Cannot move hero, destination object is busy!");
if (objectToVisit &&
objectToVisit->getOwner().isValidPlayer() &&
getPlayerRelations(objectToVisit->getOwner(), h->getOwner()) == PlayerRelations::ENEMIES &&
!turnOrder->isContactAllowed(objectToVisit->getOwner(), h->getOwner()))
return complainRet("Cannot move hero, destination player is busy!");
//it's a rock or blocked and not visitable tile
//OR hero is on land and dest is water and (there is not present only one object - boat)
if (!t.terType->isPassable() || (standAtObstacle && !canFly))
complainRet("Cannot move hero, destination tile is blocked!");
return complainRet("Cannot move hero, destination tile is blocked!");
//hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276)
if(standAtWater && !canFly && !canWalkOnSea)
complainRet("Cannot move hero, destination tile is on water!");
return complainRet("Cannot move hero, destination tile is on water!");
if(h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked)
complainRet("Cannot disembark hero, tile is blocked!");
return complainRet("Cannot disembark hero, tile is blocked!");
if(distance(h->pos, dst) >= 1.5 && !teleporting)
complainRet("Tiles are not neighboring!");
return complainRet("Tiles are not neighboring!");
if(h->inTownGarrison)
complainRet("Can not move garrisoned hero!");
return complainRet("Can not move garrisoned hero!");
if(h->movementPointsRemaining() < cost && dst != h->pos && !teleporting)
complainRet("Hero doesn't have any movement points left!");
return complainRet("Hero doesn't have any movement points left!");
if (transit && !canFly && !(canWalkOnSea && t.terType->isWater()))
complainRet("Hero cannot transit over this tile!");
if (transit && !canFly && !(canWalkOnSea && t.terType->isWater()) && !CGTeleport::isTeleport(objectToVisit))
return complainRet("Hero cannot transit over this tile!");
//several generic blocks of code
@ -1173,14 +1195,13 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
tmh.result = result;
sendAndApply(&tmh);
if (visitDest == VISIT_DEST && t.topVisitableObj() && t.topVisitableObj()->id == h->id)
if (visitDest == VISIT_DEST && objectToVisit && objectToVisit->id == h->id)
{ // Hero should be always able to visit any object he staying on even if there guards around
visitObjectOnTile(t, h);
}
else if (lookForGuards == CHECK_FOR_GUARDS && isInTheMap(guardPos))
{
const TerrainTile &guardTile = *gs->getTile(guardPos);
objectVisited(guardTile.visitableObjects.back(), h);
objectVisited(guardian, h);
moveQuery->visitDestAfterVictory = visitDest==VISIT_DEST;
}
@ -1238,9 +1259,9 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
// visit town for town portal \ castle gates
// do not use generic visitObjectOnTile to avoid double-teleporting
// if this moveHero call was triggered by teleporter
if (!t.visitableObjects.empty())
if (objectToVisit)
{
if (CGTownInstance * town = dynamic_cast<CGTownInstance *>(t.visitableObjects.back()))
if (CGTownInstance * town = dynamic_cast<CGTownInstance *>(objectToVisit))
town->onHeroVisit(h);
}
@ -1258,7 +1279,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
EVisitDest visitDest = VISIT_DEST;
if (transit)
{
if (CGTeleport::isTeleport(t.topVisitableObj()))
if (CGTeleport::isTeleport(objectToVisit))
visitDest = DONT_VISIT_DEST;
if (canFly || (canWalkOnSea && t.terType->isWater()))
@ -1271,7 +1292,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
return true;
if(h->boat && !h->boat->onboardAssaultAllowed)
lookForGuards = IGNORE_GUARDS;
lookForGuards = IGNORE_GUARDS;
turnTimerHandler.setEndTurnAllowed(h->getOwner(), !standAtWater && !standAtObstacle);
doMove(TryMoveHero::SUCCESS, lookForGuards, visitDest, LEAVING_TILE);
@ -2197,6 +2218,11 @@ bool CGameHandler::hasPlayerAt(PlayerColor player, std::shared_ptr<CConnection>
return connections.at(player).count(c);
}
bool CGameHandler::hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const
{
return connections.at(left) == connections.at(right);
}
bool CGameHandler::disbandCreature(ObjectInstanceID id, SlotID pos)
{
const CArmedInstance * s1 = static_cast<const CArmedInstance *>(getObjInstance(id));
@ -2422,6 +2448,7 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst
}
else
{
COMPLAIN_RET_FALSE_IF(getVisitingHero(dwelling) != hero, "Cannot recruit: can only recruit by visiting hero!");
COMPLAIN_RET_FALSE_IF(!hero || hero->getOwner() != player, "Cannot recruit: can only recruit to owned hero!");
}
@ -3124,12 +3151,13 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, ui8 formation)
return true;
}
bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor player)
bool CGameHandler::queryReply(QueryID qid, std::optional<int32_t> answer, PlayerColor player)
{
boost::unique_lock<boost::recursive_mutex> lock(gsm);
logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid);
logGlobal->trace(answer.toJson());
if (answer)
logGlobal->trace("%d", *answer);
auto topQuery = queries->topQuery(player);
@ -3388,6 +3416,12 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta
logGlobal->debug("%s visits %s (%d:%d)", h->nodeName(), obj->getObjectName(), obj->ID, obj->subID);
if (getVisitingHero(obj) != nullptr)
{
logGlobal->error("Attempt to visit object that is being visited by another hero!");
throw std::runtime_error("Can not visit object that is being visited");
}
std::shared_ptr<CObjectVisitQuery> visitQuery;
auto startVisit = [&](ObjectVisitStarted & event)
@ -4087,8 +4121,28 @@ void CGameHandler::changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor p
sendAndApply(&fow);
}
const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj)
{
assert(obj);
for (auto const & query : queries->allQueries())
{
auto visit = std::dynamic_pointer_cast<const CObjectVisitQuery>(query);
if (visit && visit->visitedObject == obj)
return visit->visitingHero;
}
return nullptr;
}
bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero)
{
assert(obj);
assert(hero);
assert(getVisitingHero(obj) == hero);
// Check top query of targeted player:
// If top query is NOT visit to targeted object then we assume that
// visitation query is covered by other query that must be answered first
if (auto topQuery = queries->topQuery(hero->getOwner()))
if (auto visit = std::dynamic_pointer_cast<const CObjectVisitQuery>(topQuery))
return !(visit->visitedObject == obj && visit->visitingHero == hero);

View File

@ -153,6 +153,8 @@ public:
void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override;
/// Returns hero that is currently visiting this object, or nullptr if no visit is active
const CGHeroInstance * getVisitingHero(const CGObjectInstance *obj);
bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override;
void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override;
void showInfoDialog(InfoWindow * iw) override;
@ -176,8 +178,9 @@ public:
void handleClientDisconnection(std::shared_ptr<CConnection> c);
void handleReceivedPack(CPackForServer * pack);
bool hasPlayerAt(PlayerColor player, std::shared_ptr<CConnection> c) const;
bool hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const;
bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player );
bool queryReply( QueryID qid, std::optional<int32_t> reply, PlayerColor player );
bool buildBoat( ObjectInstanceID objid, PlayerColor player );
bool setFormation( ObjectInstanceID hid, ui8 formation );
bool tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2);

View File

@ -87,6 +87,7 @@ public:
virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override;
virtual void visitLobbySetPlayer(LobbySetPlayer & pack) override;
virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) override;
virtual void visitLobbySetSimturns(LobbySetSimturns & pack) override;
virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) override;
virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) override;
};
};

View File

@ -391,6 +391,12 @@ void ApplyOnServerNetPackVisitor::visitLobbySetPlayer(LobbySetPlayer & pack)
result = true;
}
void ApplyOnServerNetPackVisitor::visitLobbySetSimturns(LobbySetSimturns & pack)
{
srv.si->simturnsInfo = pack.simturnsInfo;
result = true;
}
void ApplyOnServerNetPackVisitor::visitLobbySetTurnTime(LobbySetTurnTime & pack)
{
srv.si->turnTimerInfo = pack.turnTimerInfo;

View File

@ -93,9 +93,9 @@ bool ServerSpellCastEnvironment::moveHero(ObjectInstanceID hid, int3 dst, bool t
return gh->moveHero(hid, dst, teleporting, false);
}
void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function<void(const JsonNode&)> callback)
void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function<void(std::optional<int32_t>)> callback)
{
auto query = std::make_shared<CGenericQuery>(gh->queries.get(), color, callback);
auto query = std::make_shared<CGenericQuery>(gh, color, callback);
request->queryID = query->queryID;
gh->queries->addQuery(query);
gh->sendAndApply(request);

View File

@ -36,7 +36,7 @@ public:
const CMap * getMap() const override;
const CGameInfoCallback * getCb() const override;
bool moveHero(ObjectInstanceID hid, int3 dst, bool teleporting) override;
void genericQuery(Query * request, PlayerColor color, std::function<void(const JsonNode &)> callback) override;
void genericQuery(Query * request, PlayerColor color, std::function<void(std::optional<int32_t>)> callback) override;
private:
CGameHandler * gh;
};
};

View File

@ -174,6 +174,14 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy
if(mapObject->ID == Obj::TAVERN)
{
const CGHeroInstance * visitor = gameHandler->getVisitingHero(mapObject);
if (!visitor || visitor->getOwner() != player)
{
gameHandler->complain("Can't buy hero in tavern not being visited!");
return false;
}
if(gameHandler->getTile(mapObject->visitablePos())->visitableObjects.back() != mapObject && gameHandler->complain("Tavern entry must be unoccupied!"))
return false;
}

View File

@ -17,6 +17,8 @@
#include "../../lib/CPlayerState.h"
#include "../../lib/NetPacks.h"
#include "../../lib/pathfinder/CPathfinder.h"
#include "../../lib/pathfinder/PathfinderOptions.h"
TurnOrderProcessor::TurnOrderProcessor(CGameHandler * owner):
gameHandler(owner)
@ -24,11 +26,125 @@ TurnOrderProcessor::TurnOrderProcessor(CGameHandler * owner):
}
bool TurnOrderProcessor::canActSimultaneously(PlayerColor active, PlayerColor waiting) const
int TurnOrderProcessor::simturnsTurnsMaxLimit() const
{
return gameHandler->getStartInfo()->simturnsInfo.optionalTurns;
}
int TurnOrderProcessor::simturnsTurnsMinLimit() const
{
return gameHandler->getStartInfo()->simturnsInfo.requiredTurns;
}
void TurnOrderProcessor::updateContactStatus()
{
blockedContacts.clear();
assert(actedPlayers.empty());
assert(actingPlayers.empty());
for (auto left : awaitingPlayers)
{
for(auto right : awaitingPlayers)
{
if (left == right)
continue;
if (computeCanActSimultaneously(left, right))
blockedContacts.push_back({left, right});
}
}
}
bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) const
{
// TODO: refactor, cleanup and optimize
boost::multi_array<bool, 3> leftReachability;
boost::multi_array<bool, 3> rightReachability;
int3 mapSize = gameHandler->getMapSize();
leftReachability.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]);
rightReachability.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]);
const auto * leftInfo = gameHandler->getPlayerState(left, false);
const auto * rightInfo = gameHandler->getPlayerState(right, false);
for(const auto & hero : leftInfo->heroes)
{
CPathsInfo out(mapSize, hero);
auto config = std::make_shared<SingleHeroPathfinderConfig>(out, gameHandler->gameState(), hero);
CPathfinder pathfinder(gameHandler->gameState(), config);
pathfinder.calculatePaths();
for (int z = 0; z < mapSize.z; ++z)
for (int y = 0; y < mapSize.y; ++y)
for (int x = 0; x < mapSize.x; ++x)
if (out.getNode({x,y,z})->reachable())
leftReachability[z][x][y] = true;
}
for(const auto & hero : rightInfo->heroes)
{
CPathsInfo out(mapSize, hero);
auto config = std::make_shared<SingleHeroPathfinderConfig>(out, gameHandler->gameState(), hero);
CPathfinder pathfinder(gameHandler->gameState(), config);
pathfinder.calculatePaths();
for (int z = 0; z < mapSize.z; ++z)
for (int y = 0; y < mapSize.y; ++y)
for (int x = 0; x < mapSize.x; ++x)
if (out.getNode({x,y,z})->reachable())
rightReachability[z][x][y] = true;
}
for (int z = 0; z < mapSize.z; ++z)
for (int y = 0; y < mapSize.y; ++y)
for (int x = 0; x < mapSize.x; ++x)
if (leftReachability[z][x][y] && rightReachability[z][x][y])
return true;
return false;
}
bool TurnOrderProcessor::isContactAllowed(PlayerColor active, PlayerColor waiting) const
{
assert(active != waiting);
return !vstd::contains(blockedContacts, PlayerPair{active, waiting});
}
bool TurnOrderProcessor::computeCanActSimultaneously(PlayerColor active, PlayerColor waiting) const
{
const auto * activeInfo = gameHandler->getPlayerState(active, false);
const auto * waitingInfo = gameHandler->getPlayerState(waiting, false);
assert(active != waiting);
assert(activeInfo);
assert(waitingInfo);
if (gameHandler->hasBothPlayersAtSameConnection(active, waiting))
{
if (!gameHandler->getStartInfo()->simturnsInfo.allowHumanWithAI)
return false;
// only one AI and one human can play simultaneoulsy from single connection
if (activeInfo->human == waitingInfo->human)
return false;
}
if (gameHandler->getDate(Date::DAY) < simturnsTurnsMinLimit())
return true;
if (gameHandler->getDate(Date::DAY) > simturnsTurnsMaxLimit())
return false;
if (playersInContact(active, waiting))
return false;
return true;
}
bool TurnOrderProcessor::mustActBefore(PlayerColor left, PlayerColor right) const
{
const auto * leftInfo = gameHandler->getPlayerState(left, false);
@ -61,7 +177,7 @@ bool TurnOrderProcessor::canStartTurn(PlayerColor which) const
for (auto player : actingPlayers)
{
if (!canActSimultaneously(player, which))
if (player != which && isContactAllowed(player, which))
return false;
}
@ -86,6 +202,7 @@ void TurnOrderProcessor::doStartNewDay()
std::swap(actedPlayers, awaitingPlayers);
gameHandler->onNewTurn();
updateContactStatus();
tryStartTurnsForPlayers();
}
@ -107,8 +224,7 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which)
pst.queryID = turnQuery->queryID;
gameHandler->sendAndApply(&pst);
assert(actingPlayers.size() == 1); // No simturns yet :(
assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin()));
assert(!actingPlayers.empty());
}
void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which)
@ -130,8 +246,6 @@ void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which)
doStartNewDay();
assert(!actingPlayers.empty());
assert(actingPlayers.size() == 1); // No simturns yet :(
assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin()));
}
void TurnOrderProcessor::addPlayer(PlayerColor which)
@ -152,8 +266,6 @@ void TurnOrderProcessor::onPlayerEndsGame(PlayerColor which)
doStartNewDay();
assert(!actingPlayers.empty());
assert(actingPlayers.size() == 1); // No simturns yet :(
assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin()));
}
bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which)
@ -188,6 +300,9 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which)
void TurnOrderProcessor::onGameStarted()
{
if (actingPlayers.empty())
updateContactStatus();
// this may be game load - send notification to players that they can act
auto actingPlayersCopy = actingPlayers;
for (auto player : actingPlayersCopy)

View File

@ -17,12 +17,41 @@ class TurnOrderProcessor : boost::noncopyable
{
CGameHandler * gameHandler;
struct PlayerPair
{
PlayerColor a;
PlayerColor b;
bool operator == (const PlayerPair & other) const
{
return (a == other.a && b == other.b) || (a == other.b && b == other.a);
}
template<typename Handler>
void serialize(Handler & h, const int version)
{
h & a;
h & b;
}
};
std::vector<PlayerPair> blockedContacts;
std::set<PlayerColor> awaitingPlayers;
std::set<PlayerColor> actingPlayers;
std::set<PlayerColor> actedPlayers;
/// Returns date on which simturns must end unconditionally
int simturnsTurnsMaxLimit() const;
/// Returns date until which simturns must play unconditionally
int simturnsTurnsMinLimit() const;
/// Returns true if players are close enough to each other for their heroes to meet on this turn
bool playersInContact(PlayerColor left, PlayerColor right) const;
/// Returns true if waiting player can act alongside with currently acting player
bool canActSimultaneously(PlayerColor active, PlayerColor waiting) const;
bool computeCanActSimultaneously(PlayerColor active, PlayerColor waiting) const;
/// Returns true if left player must act before right player
bool mustActBefore(PlayerColor left, PlayerColor right) const;
@ -33,6 +62,8 @@ class TurnOrderProcessor : boost::noncopyable
/// Starts turn for all players that can start turn
void tryStartTurnsForPlayers();
void updateContactStatus();
void doStartNewDay();
void doStartPlayerTurn(PlayerColor which);
void doEndPlayerTurn(PlayerColor which);
@ -44,6 +75,8 @@ class TurnOrderProcessor : boost::noncopyable
public:
TurnOrderProcessor(CGameHandler * owner);
bool isContactAllowed(PlayerColor left, PlayerColor right) const;
/// Add new player to handle (e.g. on game start)
void addPlayer(PlayerColor which);
@ -59,6 +92,7 @@ public:
template<typename Handler>
void serialize(Handler & h, const int version)
{
h & blockedContacts;
h & awaitingPlayers;
h & actingPlayers;
h & actedPlayers;

View File

@ -26,7 +26,7 @@ void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisi
}
CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi):
CGhQuery(owner),
CQuery(owner),
battleID(bi->getBattleID())
{
belligerents[0] = bi->getSideArmy(0);
@ -37,7 +37,7 @@ CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi):
}
CBattleQuery::CBattleQuery(CGameHandler * owner):
CGhQuery(owner)
CQuery(owner)
{
belligerents[0] = belligerents[1] = nullptr;
}

View File

@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN
class IBattleInfo;
VCMI_LIB_NAMESPACE_END
class CBattleQuery : public CGhQuery
class CBattleQuery : public CQuery
{
public:
std::array<const CArmedInstance *,2> belligerents;

View File

@ -45,8 +45,9 @@ std::ostream & operator<<(std::ostream & out, QueryPtr query)
return out << "[" << query.get() << "] " << query->toString();
}
CQuery::CQuery(QueriesProcessor * Owner):
owner(Owner)
CQuery::CQuery(CGameHandler * gameHandler)
: owner(gameHandler->queries.get())
, gh(gameHandler)
{
boost::unique_lock<boost::mutex> l(QueriesProcessor::mx);
@ -127,7 +128,7 @@ void CQuery::onAdded(PlayerColor color)
}
void CQuery::setReply(const JsonNode & reply)
void CQuery::setReply(std::optional<int32_t> reply)
{
}
@ -141,14 +142,8 @@ bool CQuery::blockAllButReply(const CPack * pack) const
return true;
}
CGhQuery::CGhQuery(CGameHandler * owner):
CQuery(owner->queries.get()), gh(owner)
{
}
CDialogQuery::CDialogQuery(CGameHandler * owner):
CGhQuery(owner)
CQuery(owner)
{
}
@ -163,14 +158,14 @@ bool CDialogQuery::blocksPack(const CPack * pack) const
return blockAllButReply(pack);
}
void CDialogQuery::setReply(const JsonNode & reply)
void CDialogQuery::setReply(std::optional<int32_t> reply)
{
if(reply.getType() == JsonNode::JsonType::DATA_INTEGER)
answer = reply.Integer();
if(reply.has_value())
answer = *reply;
}
CGenericQuery::CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function<void(const JsonNode &)> Callback):
CQuery(Owner), callback(Callback)
CGenericQuery::CGenericQuery(CGameHandler * gh, PlayerColor color, std::function<void(std::optional<int32_t>)> Callback):
CQuery(gh), callback(Callback)
{
addPlayer(color);
}
@ -190,7 +185,7 @@ void CGenericQuery::onExposure(QueryPtr topQuery)
//do nothing
}
void CGenericQuery::setReply(const JsonNode & reply)
void CGenericQuery::setReply(std::optional<int32_t> reply)
{
this->reply = reply;
}

View File

@ -10,7 +10,6 @@
#pragma once
#include "../../lib/GameConstants.h"
#include "../../lib/JsonNode.h"
VCMI_LIB_NAMESPACE_BEGIN
@ -39,8 +38,7 @@ public:
std::vector<PlayerColor> players; //players that are affected (often "blocked") by query
QueryID queryID;
CQuery(QueriesProcessor * Owner);
CQuery(CGameHandler * gh);
virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle.
@ -53,11 +51,12 @@ public:
virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const;
virtual void setReply(const JsonNode & reply);
virtual void setReply(std::optional<int32_t> reply);
virtual ~CQuery();
protected:
QueriesProcessor * owner;
CGameHandler * gh;
void addPlayer(PlayerColor color);
bool blockAllButReply(const CPack * pack) const;
};
@ -65,21 +64,13 @@ protected:
std::ostream &operator<<(std::ostream &out, const CQuery &query);
std::ostream &operator<<(std::ostream &out, QueryPtr query);
class CGhQuery : public CQuery
{
public:
CGhQuery(CGameHandler * owner);
protected:
CGameHandler * gh;
};
class CDialogQuery : public CGhQuery
class CDialogQuery : public CQuery
{
public:
CDialogQuery(CGameHandler * owner);
virtual bool endsByPlayerAnswer() const override;
virtual bool blocksPack(const CPack *pack) const override;
void setReply(const JsonNode & reply) override;
void setReply(std::optional<int32_t> reply) override;
protected:
std::optional<ui32> answer;
};
@ -87,14 +78,14 @@ protected:
class CGenericQuery : public CQuery
{
public:
CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function<void(const JsonNode &)> Callback);
CGenericQuery(CGameHandler * gh, PlayerColor color, std::function<void(std::optional<int32_t>)> Callback);
bool blocksPack(const CPack * pack) const override;
bool endsByPlayerAnswer() const override;
void onExposure(QueryPtr topQuery) override;
void setReply(const JsonNode & reply) override;
void setReply(std::optional<int32_t> reply) override;
void onRemoval(PlayerColor color) override;
private:
std::function<void(const JsonNode &)> callback;
JsonNode reply;
std::function<void(std::optional<int32_t>)> callback;
std::optional<int32_t> reply;
};

View File

@ -16,7 +16,7 @@
#include "../../lib/serializer/Cast.h"
PlayerStartsTurnQuery::PlayerStartsTurnQuery(CGameHandler * owner, PlayerColor player):
CGhQuery(owner)
CQuery(owner)
{
addPlayer(player);
}
@ -42,7 +42,7 @@ bool PlayerStartsTurnQuery::endsByPlayerAnswer() const
}
CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile):
CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false)
CQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false)
{
addPlayer(Hero->tempOwner);
}
@ -213,7 +213,7 @@ void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQu
}
CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory):
CGhQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero)
CQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero)
{
players.push_back(hero->tempOwner);
}

View File

@ -17,7 +17,7 @@ class TurnTimerHandler;
//Created when player starts turn
//Removed when player accepts a turn
class PlayerStartsTurnQuery : public CGhQuery
class PlayerStartsTurnQuery : public CQuery
{
public:
PlayerStartsTurnQuery(CGameHandler * owner, PlayerColor player);
@ -30,7 +30,7 @@ public:
//Created when hero visits object.
//Removed when query above is resolved (or immediately after visit if no queries were created)
class CObjectVisitQuery : public CGhQuery
class CObjectVisitQuery : public CQuery
{
public:
const CGObjectInstance *visitedObject;
@ -47,7 +47,7 @@ public:
//Created when hero attempts move and something happens
//(not necessarily position change, could be just an object interaction).
class CHeroMovementQuery : public CGhQuery
class CHeroMovementQuery : public CQuery
{
public:
TryMoveHero tmh;

View File

@ -126,7 +126,7 @@ public:
return false;
}
void genericQuery(Query * request, PlayerColor color, std::function<void(const JsonNode &)> callback) override
void genericQuery(Query * request, PlayerColor color, std::function<void(std::optional<int32_t>)> callback) override
{
//todo:
}