Schedule turn-start hero visits on towns; fixes multiple hero level-ups on map start with more than 2 towns with built War Academies

This commit is contained in:
Michał Zaremba
2026-06-07 19:22:48 +02:00
parent 7a40b4635e
commit 2ac4942dd1
9 changed files with 161 additions and 5 deletions
+6
View File
@@ -14,6 +14,7 @@
#include "TurnTimerHandler.h" #include "TurnTimerHandler.h"
#include "ServerNetPackVisitors.h" #include "ServerNetPackVisitors.h"
#include "ServerSpellCastEnvironment.h" #include "ServerSpellCastEnvironment.h"
#include "TurnStartVisitScheduler.h"
#include "battles/BattleProcessor.h" #include "battles/BattleProcessor.h"
#include "processors/HeroPoolProcessor.h" #include "processors/HeroPoolProcessor.h"
#include "processors/NewTurnProcessor.h" #include "processors/NewTurnProcessor.h"
@@ -542,6 +543,7 @@ CGameHandler::CGameHandler(IGameServer & server)
, heroPool(std::make_unique<HeroPoolProcessor>(this)) , heroPool(std::make_unique<HeroPoolProcessor>(this))
, battles(std::make_unique<BattleProcessor>(this)) , battles(std::make_unique<BattleProcessor>(this))
, queries(std::make_unique<QueriesProcessor>()) , queries(std::make_unique<QueriesProcessor>())
, turnStartVisitScheduler(std::make_unique<TurnStartVisitScheduler>(*this, *queries))
, turnOrder(std::make_unique<TurnOrderProcessor>(this)) , turnOrder(std::make_unique<TurnOrderProcessor>(this))
, turnTimerHandler(std::make_unique<TurnTimerHandler>(*this)) , turnTimerHandler(std::make_unique<TurnTimerHandler>(*this))
, newTurnProcessor(std::make_unique<NewTurnProcessor>(this)) , newTurnProcessor(std::make_unique<NewTurnProcessor>(this))
@@ -553,6 +555,7 @@ CGameHandler::CGameHandler(IGameServer & server)
, complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!")
, complainInvalidSlot("Invalid slot accessed!") , complainInvalidSlot("Invalid slot accessed!")
{ {
queries->setListener(turnStartVisitScheduler.get());
} }
CGameHandler::~CGameHandler() = default; CGameHandler::~CGameHandler() = default;
@@ -4216,6 +4219,9 @@ bool CGameHandler::isBlockedByQueries(const CPackForServer *pack, PlayerColor pl
if (dynamic_cast<const SaveLocalState *>(pack) != nullptr) if (dynamic_cast<const SaveLocalState *>(pack) != nullptr)
return false; return false;
if(dynamic_cast<const AdvInterfaceReady *>(pack) != nullptr)
return false;
auto query = queries->topQuery(player); auto query = queries->topQuery(player);
if (query && query->blocksPack(pack)) if (query && query->blocksPack(pack))
{ {
+2
View File
@@ -51,6 +51,7 @@ class QueriesProcessor;
class CObjectVisitQuery; class CObjectVisitQuery;
class NewTurnProcessor; class NewTurnProcessor;
class IGameServer; class IGameServer;
class TurnStartVisitScheduler;
class CGameHandler : public Environment, public IGameEventCallback class CGameHandler : public Environment, public IGameEventCallback
{ {
@@ -60,6 +61,7 @@ public:
std::unique_ptr<HeroPoolProcessor> heroPool; std::unique_ptr<HeroPoolProcessor> heroPool;
std::unique_ptr<BattleProcessor> battles; std::unique_ptr<BattleProcessor> battles;
std::unique_ptr<QueriesProcessor> queries; std::unique_ptr<QueriesProcessor> queries;
std::unique_ptr<TurnStartVisitScheduler> turnStartVisitScheduler;
std::unique_ptr<TurnOrderProcessor> turnOrder; std::unique_ptr<TurnOrderProcessor> turnOrder;
std::unique_ptr<TurnTimerHandler> turnTimerHandler; std::unique_ptr<TurnTimerHandler> turnTimerHandler;
std::unique_ptr<NewTurnProcessor> newTurnProcessor; std::unique_ptr<NewTurnProcessor> newTurnProcessor;
+3
View File
@@ -24,6 +24,7 @@ set(vcmiservercommon_SRCS
NetPacksServer.cpp NetPacksServer.cpp
NetPacksLobbyServer.cpp NetPacksLobbyServer.cpp
TurnTimerHandler.cpp TurnTimerHandler.cpp
TurnStartVisitScheduler.cpp
) )
set(vcmiservercommon_HEADERS set(vcmiservercommon_HEADERS
@@ -39,6 +40,7 @@ set(vcmiservercommon_HEADERS
queries/MapQueries.h queries/MapQueries.h
queries/VisitQueries.h queries/VisitQueries.h
queries/QueriesProcessor.h queries/QueriesProcessor.h
queries/IQueryStackListener.h
processors/HeroPoolProcessor.h processors/HeroPoolProcessor.h
processors/NewTurnProcessor.h processors/NewTurnProcessor.h
@@ -53,6 +55,7 @@ set(vcmiservercommon_HEADERS
LobbyNetPackVisitors.h LobbyNetPackVisitors.h
ServerNetPackVisitors.h ServerNetPackVisitors.h
TurnTimerHandler.h TurnTimerHandler.h
TurnStartVisitScheduler.h
) )
assign_source_group(${vcmiservercommon_SRCS} ${vcmiservercommon_HEADERS}) assign_source_group(${vcmiservercommon_SRCS} ${vcmiservercommon_HEADERS})
+60
View File
@@ -0,0 +1,60 @@
/*
* TurnStartVisitScheduler.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "TurnStartVisitScheduler.h"
#include "CGameHandler.h"
#include "lib/gameState/CGameState.h"
#include "queries/QueriesProcessor.h"
TurnStartVisitScheduler::TurnStartVisitScheduler(CGameHandler & gameHandler, QueriesProcessor & queries)
: gameHandler(gameHandler)
, queries(queries)
{
}
void TurnStartVisitScheduler::enqueue(PlayerColor player, std::deque<PendingTurnStartVisit> visits)
{
sessions[player] = std::move(visits);
}
void TurnStartVisitScheduler::processNext(PlayerColor player)
{
auto & session = sessions[player];
if(session.empty())
return;
if(queries.topQuery(player))
return;
if(!session.empty())
{
const auto next = session.front();
session.pop_front();
const auto * object = gameHandler.gameState().getObjInstance(next.objectId);
const auto * hero = gameHandler.gameState().getHero(next.heroId);
gameHandler.objectVisited(object, hero);
}
}
void TurnStartVisitScheduler::onQueryStackChanged(PlayerColor player)
{
processNext(player);
}
void TurnStartVisitScheduler::clear(PlayerColor player)
{
sessions[player].clear();
}
+41
View File
@@ -0,0 +1,41 @@
/*
* TurnStartVisitScheduler.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "queries/IQueryStackListener.h"
class CGameHandler;
class QueriesProcessor;
struct PendingTurnStartVisit
{
PlayerColor player;
ObjectInstanceID objectId;
ObjectInstanceID heroId;
};
// Schedules deferred turn-start object visits and runs them one at a time
// per player, resuming only after the previous visit's query chain finishes.
class TurnStartVisitScheduler final : public IQueryStackListener
{
public:
TurnStartVisitScheduler(CGameHandler & gameHandler, QueriesProcessor & queries);
void enqueue(PlayerColor player, std::deque<PendingTurnStartVisit> visits);
void processNext(PlayerColor player);
void onQueryStackChanged(PlayerColor player) override;
void clear(PlayerColor player);
private:
CGameHandler & gameHandler;
QueriesProcessor & queries;
std::array<std::deque<PendingTurnStartVisit>, PlayerColor::PLAYER_LIMIT_I> sessions;
};
+10 -4
View File
@@ -34,6 +34,7 @@
#include "../../lib/networkPacks/StackLocation.h" #include "../../lib/networkPacks/StackLocation.h"
#include "../../lib/pathfinder/TurnInfo.h" #include "../../lib/pathfinder/TurnInfo.h"
#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/texts/CGeneralTextHandler.h"
#include "../TurnStartVisitScheduler.h"
#include <vstd/RNG.h> #include <vstd/RNG.h>
@@ -149,15 +150,20 @@ void NewTurnProcessor::onPlayerTurnStarted(PlayerColor which)
for (const auto * t : playerState->getTowns()) for (const auto * t : playerState->getTowns())
handleTownEvents(t); handleTownEvents(t);
std::deque<PendingTurnStartVisit> visits;
for (const auto * t : playerState->getTowns()) for (const auto * t : playerState->getTowns())
{ {
//garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
if (t->getGarrisonHero() != nullptr) if(t->getGarrisonHero() != nullptr)
gameHandler->objectVisited(t, t->getGarrisonHero()); visits.push_back({which, t->id, t->getGarrisonHero()->id});
if (t->getVisitingHero() != nullptr) if(t->getVisitingHero() != nullptr)
gameHandler->objectVisited(t, t->getVisitingHero()); visits.push_back({which, t->id, t->getVisitingHero()->id});
} }
gameHandler->turnStartVisitScheduler->enqueue(which, std::move(visits));
gameHandler->turnStartVisitScheduler->processNext(which);
} }
void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which) void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which)
+22
View File
@@ -0,0 +1,22 @@
/*
* IQueryStackListener.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../../lib/constants/EntityIdentifiers.h"
class IQueryStackListener
{
public:
virtual ~IQueryStackListener() = default;
// Called after a player's query stack changes and local query lifecycle
// handling for that operation is complete.
virtual void onQueryStackChanged(PlayerColor player) = 0;
};
+11
View File
@@ -33,6 +33,9 @@ void QueriesProcessor::popQuery(PlayerColor player, QueryPtr query)
//Exposure on query below happens only if removal didn't trigger any new query //Exposure on query below happens only if removal didn't trigger any new query
if(nextQuery && nextQuery == topQuery(player)) if(nextQuery && nextQuery == topQuery(player))
nextQuery->onExposure(query); nextQuery->onExposure(query);
if(queriesStackListener)
queriesStackListener->onQueryStackChanged(player);
} }
void QueriesProcessor::popQuery(const CQuery &query) void QueriesProcessor::popQuery(const CQuery &query)
@@ -82,6 +85,9 @@ void QueriesProcessor::addQuery(PlayerColor player, QueryPtr query)
query->onAdding(player); query->onAdding(player);
queries[player].push_back(query); queries[player].push_back(query);
query->onAdded(player); query->onAdded(player);
if(queriesStackListener)
queriesStackListener->onQueryStackChanged(player);
} }
QueryPtr QueriesProcessor::topQuery(PlayerColor player) QueryPtr QueriesProcessor::topQuery(PlayerColor player)
@@ -145,3 +151,8 @@ int QueriesProcessor::countQuery(const QueryPtr & query) const
return result; return result;
} }
void QueriesProcessor::setListener(IQueryStackListener * listener)
{
queriesStackListener = listener;
}
+6 -1
View File
@@ -12,6 +12,7 @@
#include "../../lib/GameConstants.h" #include "../../lib/GameConstants.h"
#include "constants/EntityIdentifiers.h" #include "constants/EntityIdentifiers.h"
#include "queries/CQuery.h" #include "queries/CQuery.h"
#include "IQueryStackListener.h"
class CQuery; class CQuery;
using QueryPtr = std::shared_ptr<CQuery>; using QueryPtr = std::shared_ptr<CQuery>;
@@ -22,11 +23,15 @@ public:
using QueriesStack = std::vector<QueryPtr>; using QueriesStack = std::vector<QueryPtr>;
using QueriesPerPlayer = std::array<QueriesStack, PlayerColor::PLAYER_LIMIT_I>; using QueriesPerPlayer = std::array<QueriesStack, PlayerColor::PLAYER_LIMIT_I>;
// Sets an optional listener notified when a player's query stack changes.
void setListener(IQueryStackListener * listener);
private: private:
void addQuery(PlayerColor player, QueryPtr query); void addQuery(PlayerColor player, QueryPtr query);
void popQuery(PlayerColor player, QueryPtr query); void popQuery(PlayerColor player, QueryPtr query);
QueriesPerPlayer queries; QueriesPerPlayer queries;
IQueryStackListener * queriesStackListener = nullptr;
template<typename StorageT> template<typename StorageT>
class AllQueriesViewT class AllQueriesViewT
@@ -55,7 +60,7 @@ private:
decltype(auto) operator*() const decltype(auto) operator*() const
{ {
return (*storage)[outer][inner]; // QueryPtr& albo const QueryPtr& return (*storage)[outer][inner]; // QueryPtr& or const QueryPtr&
} }
iterator & operator++() iterator & operator++()