1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-25 22:42:04 +02:00

Merge pull request #5859 from IvanSavenko/antilag

Lag compensation for multiplayer
This commit is contained in:
Ivan Savenko
2025-07-30 12:02:36 +03:00
committed by GitHub
78 changed files with 1350 additions and 579 deletions

View File

@@ -75,7 +75,6 @@
#include "../lib/serializer/CSaveFile.h"
#include "../lib/serializer/CLoadFile.h"
#include "../lib/serializer/Connection.h"
#include "../lib/spells/CSpellHandler.h"
@@ -128,9 +127,9 @@ events::EventBus * CGameHandler::eventBus() const
return serverEventBus.get();
}
CVCMIServer & CGameHandler::gameLobby() const
IGameServer & CGameHandler::gameServer() const
{
return *lobby;
return server;
}
void CGameHandler::levelUpHero(const CGHeroInstance * hero, SecondarySkill skill)
@@ -437,58 +436,64 @@ void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill wh
}
void CGameHandler::handleClientDisconnection(const std::shared_ptr<CConnection> & c)
void CGameHandler::handleClientDisconnection(GameConnectionID connectionID)
{
if(gameLobby().getState() == EServerState::SHUTDOWN || !gameState().getStartInfo())
if(gameServer().getState() == EServerState::SHUTDOWN || !gameState().getStartInfo())
{
assert(0); // game should have shut down before reaching this point!
return;
}
for(auto & playerConnections : connections)
{
PlayerColor playerId = playerConnections.first;
auto playerConnection = vstd::find(playerConnections.second, c);
if(playerConnection == playerConnections.second.end())
std::vector<PlayerColor> disconnectedPlayers;
std::vector<PlayerColor> remainingPlayers;
// this player have left the game - broadcast infowindow to all in-game players
for (const auto & player : gameState().players)
{
if (gameInfo().getPlayerState(player.first)->status != EPlayerStatus::INGAME)
continue;
logGlobal->trace("Player %s disconnected. Notifying remaining players", playerId.toString());
if (gameServer().hasPlayerAt(player.first, connectionID))
disconnectedPlayers.push_back(player.first);
else
remainingPlayers.push_back(player.first);
}
// this player have left the game - broadcast infowindow to all in-game players
for (const auto & player : gameState().players)
for (const auto & inGamePlayer : remainingPlayers)
{
for (const auto & lostPlayer : disconnectedPlayers)
{
if (player.first == playerId)
continue;
if (gameInfo().getPlayerState(player.first)->status != EPlayerStatus::INGAME)
continue;
logGlobal->trace("Notifying player %s", player.first);
InfoWindow out;
out.player = player.first;
out.player = inGamePlayer;
out.text.appendTextID("vcmi.server.errors.playerLeft");
out.text.replaceName(playerId);
out.components.emplace_back(ComponentType::FLAG, playerId);
out.text.replaceName(lostPlayer);
out.components.emplace_back(ComponentType::FLAG, lostPlayer);
sendAndApply(out);
}
}
}
void CGameHandler::handleReceivedPack(std::shared_ptr<CConnection> connection, CPackForServer & pack)
void CGameHandler::handleReceivedPack(GameConnectionID connection, CPackForServer & pack)
{
//prepare struct informing that action was applied
auto sendPackageResponse = [&](bool successfullyApplied)
{
PackageApplied applied;
applied.player = pack.player;
applied.result = successfullyApplied;
applied.packType = CTypeList::getInstance().getTypeID(&pack);
applied.requestID = pack.requestID;
connection->sendPack(applied);
PackageApplied applied(
pack.player,
pack.requestID,
CTypeList::getInstance().getTypeID(&pack),
successfullyApplied
);
gameServer().sendPack(applied, connection);
};
PackageReceived received(
pack.player,
pack.requestID,
CTypeList::getInstance().getTypeID(&pack)
);
gameServer().sendPack(received, connection);
if(isBlockedByQueries(&pack, pack.player))
{
sendPackageResponse(false);
@@ -517,8 +522,15 @@ void CGameHandler::handleReceivedPack(std::shared_ptr<CConnection> connection, C
}
}
CGameHandler::CGameHandler(CVCMIServer * lobby)
: lobby(lobby)
CGameHandler::CGameHandler(IGameServer & server, const std::shared_ptr<CGameState> & initialGamestate)
:CGameHandler(server)
{
gs = initialGamestate;
randomizer = std::make_unique<GameRandomizer>(*gs);
}
CGameHandler::CGameHandler(IGameServer & server)
: server(server)
, heroPool(std::make_unique<HeroPoolProcessor>(this))
, battles(std::make_unique<BattleProcessor>(this))
, queries(std::make_unique<QueriesProcessor>())
@@ -732,19 +744,6 @@ void CGameHandler::start(bool resume)
{
LOG_TRACE_PARAMS(logGlobal, "resume=%d", resume);
for (const auto & cc : gameLobby().activeConnections)
{
auto players = gameLobby().getAllClientPlayers(cc->connectionID);
std::stringstream sbuffer;
sbuffer << "Connection " << cc->connectionID << " will handle " << players.size() << " player: ";
for (PlayerColor color : players)
{
sbuffer << color << " ";
connections[color].insert(cc);
}
logGlobal->info(sbuffer.str());
}
#if SCRIPTING_ENABLED
services()->scripts()->run(serverScripts);
#endif
@@ -1129,7 +1128,7 @@ void CGameHandler::showBlockingDialog(const IObjectInterface * caller, BlockingD
auto dialogQuery = std::make_shared<CBlockingDialogQuery>(this, caller, *iw);
queries->addQuery(dialogQuery);
iw->queryID = dialogQuery->queryID;
sendToAllClients(*iw);
sendAndApply(*iw);
}
void CGameHandler::showTeleportDialog(TeleportDialog *iw)
@@ -1137,7 +1136,7 @@ void CGameHandler::showTeleportDialog(TeleportDialog *iw)
auto dialogQuery = std::make_shared<CTeleportDialogQuery>(this, *iw);
queries->addQuery(dialogQuery);
iw->queryID = dialogQuery->queryID;
sendToAllClients(*iw);
sendAndApply(*iw);
}
void CGameHandler::giveResource(PlayerColor player, GameResID which, int val)
@@ -1502,18 +1501,9 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)
}
}
void CGameHandler::sendToAllClients(const CPackForClient & pack)
{
logNetwork->trace("\tSending to all clients: %s", typeid(pack).name());
for (const auto & c : gameLobby().activeConnections)
c->sendPack(pack);
}
void CGameHandler::sendAndApply(CPackForClient & pack)
{
sendToAllClients(pack);
gs->apply(pack);
logNetwork->trace("\tApplied on gameState(): %s", typeid(pack).name());
gameServer().applyPack(pack);
}
void CGameHandler::sendAndApply(CGarrisonOperationPack & pack)
@@ -1534,62 +1524,62 @@ void CGameHandler::sendAndApply(NewStructures & pack)
checkVictoryLossConditionsForPlayer(gameInfo().getTown(pack.tid)->tempOwner);
}
bool CGameHandler::isPlayerOwns(const std::shared_ptr<CConnection> & connection, const CPackForServer * pack, ObjectInstanceID id)
bool CGameHandler::isPlayerOwns(GameConnectionID connectionID, const CPackForServer * pack, ObjectInstanceID id)
{
return pack->player == gameState().getOwner(id) && hasPlayerAt(gameState().getOwner(id), connection);
return pack->player == gameState().getOwner(id) && hasPlayerAt(gameState().getOwner(id), connectionID);
}
void CGameHandler::throwNotAllowedAction(const std::shared_ptr<CConnection> & connection)
void CGameHandler::throwNotAllowedAction(GameConnectionID connectionID)
{
playerMessages->sendSystemMessage(connection, MetaString::createFromTextID("vcmi.server.errors.notAllowed"));
playerMessages->sendSystemMessage(connectionID, MetaString::createFromTextID("vcmi.server.errors.notAllowed"));
logNetwork->error("Player is not allowed to perform this action!");
throw ExceptionNotAllowedAction();
}
void CGameHandler::wrongPlayerMessage(const std::shared_ptr<CConnection> & connection, const CPackForServer * pack, PlayerColor expectedplayer)
void CGameHandler::wrongPlayerMessage(GameConnectionID connectionID, const CPackForServer * pack, PlayerColor expectedplayer)
{
auto str = MetaString::createFromTextID("vcmi.server.errors.wrongIdentified");
str.replaceName(pack->player);
str.replaceName(expectedplayer);
logNetwork->error(str.toString());
playerMessages->sendSystemMessage(connection, str);
playerMessages->sendSystemMessage(connectionID, str);
}
void CGameHandler::throwIfWrongOwner(const std::shared_ptr<CConnection> & connection, const CPackForServer * pack, ObjectInstanceID id)
void CGameHandler::throwIfWrongOwner(GameConnectionID connectionID, const CPackForServer * pack, ObjectInstanceID id)
{
if(!isPlayerOwns(connection, pack, id))
if(!isPlayerOwns(connectionID, pack, id))
{
wrongPlayerMessage(connection, pack, gameState().getOwner(id));
throwNotAllowedAction(connection);
wrongPlayerMessage(connectionID, pack, gameState().getOwner(id));
throwNotAllowedAction(connectionID);
}
}
void CGameHandler::throwIfPlayerNotActive(const std::shared_ptr<CConnection> & connection, const CPackForServer * pack)
void CGameHandler::throwIfPlayerNotActive(GameConnectionID connectionID, const CPackForServer * pack)
{
if (!turnOrder->isPlayerMakingTurn(pack->player))
throwNotAllowedAction(connection);
if (!vstd::contains(gs->actingPlayers, pack->player))
throwNotAllowedAction(connectionID);
}
void CGameHandler::throwIfWrongPlayer(const std::shared_ptr<CConnection> & connection, const CPackForServer * pack)
void CGameHandler::throwIfWrongPlayer(GameConnectionID connectionID, const CPackForServer * pack)
{
throwIfWrongPlayer(connection, pack, pack->player);
throwIfWrongPlayer(connectionID, pack, pack->player);
}
void CGameHandler::throwIfWrongPlayer(const std::shared_ptr<CConnection> & connection, const CPackForServer * pack, PlayerColor player)
void CGameHandler::throwIfWrongPlayer(GameConnectionID connectionID, const CPackForServer * pack, PlayerColor player)
{
if(!hasPlayerAt(player, connection) || pack->player != player)
if(!hasPlayerAt(player, connectionID) || pack->player != player)
{
wrongPlayerMessage(connection, pack, player);
throwNotAllowedAction(connection);
wrongPlayerMessage(connectionID, pack, player);
throwNotAllowedAction(connectionID);
}
}
void CGameHandler::throwAndComplain(const std::shared_ptr<CConnection> & connection, const std::string & txt)
void CGameHandler::throwAndComplain(GameConnectionID connectionID, const std::string & txt)
{
complain(txt);
throwNotAllowedAction(connection);
throwNotAllowedAction(connectionID);
}
void CGameHandler::save(const std::string & filename)
@@ -1614,64 +1604,23 @@ void CGameHandler::save(const std::string & filename)
}
}
bool CGameHandler::load(const std::string & filename)
void CGameHandler::load(const StartInfo &info)
{
logGlobal->info("Loading from %s", filename);
const auto stem = FileInfo::GetPathStem(filename);
logGlobal->info("Loading from %s", info.fileURI);
const auto stem = FileInfo::GetPathStem(info.fileURI);
reinitScripting();
try
{
CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(stem.to_string(), EResType::SAVEGAME)), gs.get());
gs = std::make_shared<CGameState>();
randomizer = std::make_unique<GameRandomizer>(*gs);
gs->loadGame(lf);
logGlobal->info("Loading server state");
lf.load(*this);
logGlobal->info("Game has been successfully loaded!");
}
catch(const ModIncompatibility & e)
{
logGlobal->error("Failed to load game: %s", e.what());
MetaString errorMsg;
if(!e.whatMissing().empty())
{
errorMsg.appendTextID("vcmi.server.errors.modsToEnable");
errorMsg.appendRawString("\n");
errorMsg.appendRawString(e.whatMissing());
}
if(!e.whatExcessive().empty())
{
errorMsg.appendTextID("vcmi.server.errors.modsToDisable");
errorMsg.appendRawString("\n");
errorMsg.appendRawString(e.whatExcessive());
}
gameLobby().announceMessage(errorMsg);
return false;
}
catch(const IdentifierResolutionException & e)
{
logGlobal->error("Failed to load game: %s", e.what());
MetaString errorMsg;
errorMsg.appendTextID("vcmi.server.errors.unknownEntity");
errorMsg.replaceRawString(e.identifierName);
gameLobby().announceMessage(errorMsg);
return false;
}
CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(stem.to_string(), EResType::SAVEGAME)), gs.get());
gs = std::make_shared<CGameState>();
randomizer = std::make_unique<GameRandomizer>(*gs);
gs->loadGame(lf);
logGlobal->info("Loading server state");
lf.load(*this);
logGlobal->info("Game has been successfully loaded!");
catch(const std::exception & e)
{
logGlobal->error("Failed to load game: %s", e.what());
auto str = MetaString::createFromTextID("vcmi.broadcast.failedLoadGame");
str.appendRawString(": ");
str.appendRawString(e.what());
gameLobby().announceMessage(str);
return false;
}
gs->preInit(LIBRARY);
gs->updateOnLoad(gameLobby().si.get());
return true;
gs->updateOnLoad(info);
}
bool CGameHandler::bulkSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner, si32 howMany)
@@ -2084,14 +2033,14 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8
return true;
}
bool CGameHandler::hasPlayerAt(PlayerColor player, const std::shared_ptr<CConnection> & c) const
bool CGameHandler::hasPlayerAt(PlayerColor player, GameConnectionID connectionID) const
{
return connections.count(player) && connections.at(player).count(c);
return gameServer().hasPlayerAt(player, connectionID);
}
bool CGameHandler::hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const
{
return connections.count(left) && connections.count(right) && connections.at(left) == connections.at(right);
return gameServer().hasBothPlayersAtSameConnection(left, right);
}
bool CGameHandler::disbandCreature(ObjectInstanceID id, SlotID pos)
@@ -3578,7 +3527,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
if(p->human)
{
gameLobby().setState(EServerState::SHUTDOWN);
gameServer().setState(EServerState::SHUTDOWN);
}
}
else
@@ -4118,7 +4067,7 @@ void CGameHandler::removeAfterVisit(const ObjectInstanceID & id)
void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode)
{
std::unordered_set<int3> tiles;
FowTilesType tiles;
if (mode == ETileVisibility::HIDDEN)
{
@@ -4131,7 +4080,7 @@ void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player,
changeFogOfWar(tiles, player, mode);
}
void CGameHandler::changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode)
void CGameHandler::changeFogOfWar(const FowTilesType &tiles, PlayerColor player, ETileVisibility mode)
{
if (tiles.empty())
return;
@@ -4144,7 +4093,7 @@ void CGameHandler::changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerC
if (mode == ETileVisibility::HIDDEN)
{
// do not hide tiles observed by owned objects. May lead to disastrous AI problems
std::unordered_set<int3> observedTiles;
FowTilesType observedTiles;
const auto * p = gameInfo().getPlayerState(player);
for (const auto * obj : p->getOwnedObjects())
gameInfo().getTilesInRange(observedTiles, obj->getSightCenter(), obj->getSightRadius(), ETileVisibility::REVEALED, obj->getOwner());