Browse Source

Add comments, rename entities to more logical names, reorganize code

Ivan Savenko 4 months ago
parent
commit
44cc3a0214

+ 0 - 334
client/AntilagServer.cpp

@@ -1,334 +0,0 @@
-/*
- * AntilagServer.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 "AntilagServer.h"
-
-#include "CServerHandler.h"
-#include "Client.h"
-#include "GameEngine.h"
-
-#include "../server/CGameHandler.h"
-#include "../lib/gameState/CGameState.h"
-#include "../lib/mapObjects/CGHeroInstance.h"
-#include "../lib/serializer/GameConnection.h"
-#include "GameInstance.h"
-
-class AntilagRollbackNotSupportedException : public std::runtime_error
-{
-public:
-	using std::runtime_error::runtime_error;
-};
-
-
-int ConnectionPackWriter::write(const std::byte * data, unsigned size)
-{
-	buffer.insert(buffer.end(), data, data + size);
-	return size;
-}
-
-void AntilagRollbackGeneratorVisitor::visitPackageReceived(PackageReceived & pack)
-{
-	success = true;
-	// no-op rollback?
-}
-
-void AntilagRollbackGeneratorVisitor::visitPackageApplied(PackageApplied & pack)
-{
-	success = true;
-	// no-op rollback?
-}
-
-void AntilagRollbackGeneratorVisitor::visitPlayerBlocked(PlayerBlocked & pack)
-{
-	success = true;
-	// no-op rollback?
-}
-
-void AntilagRollbackGeneratorVisitor::visitSwapStacks(SwapStacks & pack)
-{
-	auto rollbackSwap = std::make_unique<SwapStacks>();
-
-	rollbackSwap->srcArmy = pack.dstArmy;
-	rollbackSwap->dstArmy = pack.srcArmy;
-	rollbackSwap->srcSlot = pack.dstSlot;
-	rollbackSwap->dstSlot = pack.srcSlot;
-
-	rollbackPacks.push_back(std::move(rollbackSwap));
-	success = true;
-}
-
-void AntilagRollbackGeneratorVisitor::visitRebalanceStacks(RebalanceStacks & pack)
-{
-	const auto * srcObject = gs.getObjInstance(pack.srcArmy);
-	const auto * dstObject = gs.getObjInstance(pack.dstArmy);
-
-	const auto * srcArmy = dynamic_cast<const CArmedInstance*>(srcObject);
-	const auto * dstArmy = dynamic_cast<const CArmedInstance*>(dstObject);
-
-	if (srcArmy->getStack(pack.srcSlot).getTotalExperience() != 0 ||
-	   dstArmy->getStack(pack.srcSlot).getTotalExperience() != 0 ||
-	   srcArmy->getStack(pack.srcSlot).getSlot(ArtifactPosition::CREATURE_SLOT)->artifactID.hasValue())
-	{
-		// TODO: rollback creature artifacts & stack experience
-		return;
-	}
-
-	auto rollbackRebalance = std::make_unique<RebalanceStacks>();
-	rollbackRebalance->srcArmy = pack.dstArmy;
-	rollbackRebalance->dstArmy = pack.srcArmy;
-	rollbackRebalance->srcSlot = pack.dstSlot;
-	rollbackRebalance->dstSlot = pack.srcSlot;
-	rollbackRebalance->count = pack.count;
-	rollbackPacks.push_back(std::move(rollbackRebalance));
-	success = true;
-}
-
-void AntilagRollbackGeneratorVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
-{
-	for (auto & subpack : pack.moves)
-		visitRebalanceStacks(subpack);
-
-	success = true;
-}
-
-void AntilagRollbackGeneratorVisitor::visitHeroVisitCastle(HeroVisitCastle & pack)
-{
-	auto rollbackVisit = std::make_unique<HeroVisitCastle>();
-	rollbackVisit->startVisit = !pack.startVisit;
-	rollbackVisit->tid = pack.tid;
-	rollbackVisit->hid = pack.hid;
-
-	rollbackPacks.push_back(std::move(rollbackVisit));
-
-	success = true;
-}
-
-void AntilagRollbackGeneratorVisitor::visitTryMoveHero(TryMoveHero & pack)
-{
-	auto rollbackMove = std::make_unique<TryMoveHero>();
-	auto rollbackFow = std::make_unique<FoWChange>();
-	const auto * movedHero = gs.getHero(pack.id);
-
-	rollbackMove->id = pack.id;
-	rollbackMove->movePoints = movedHero->movementPointsRemaining();
-	rollbackMove->result = pack.result;
-	if (pack.result == TryMoveHero::EMBARK)
-		rollbackMove->result = TryMoveHero::DISEMBARK;
-	if (pack.result == TryMoveHero::DISEMBARK)
-		rollbackMove->result = TryMoveHero::EMBARK;
-	rollbackMove->start = pack.end;
-	rollbackMove->end = pack.start;
-
-	rollbackFow->mode = ETileVisibility::HIDDEN;
-	rollbackFow->player = movedHero->getOwner();
-	rollbackFow->tiles = pack.fowRevealed;
-
-	rollbackPacks.push_back(std::move(rollbackMove));
-	rollbackPacks.push_back(std::move(rollbackFow));
-	success = true;
-}
-
-bool AntilagRollbackGeneratorVisitor::canBeRolledBack() const
-{
-	return success;
-}
-
-std::vector<std::unique_ptr<CPackForClient>> AntilagRollbackGeneratorVisitor::getRollbackPacks()
-{
-	return std::move(rollbackPacks);
-}
-
-AntilagReplyPredictionVisitor::AntilagReplyPredictionVisitor() = default;
-
-void AntilagReplyPredictionVisitor::visitMoveHero(MoveHero & pack)
-{
-	canBeAppliedValue = true;
-}
-
-void AntilagReplyPredictionVisitor::visitArrangeStacks(ArrangeStacks & pack)
-{
-	canBeAppliedValue = true;
-}
-
-bool AntilagReplyPredictionVisitor::canBeApplied() const
-{
-	return canBeAppliedValue;
-}
-
-AntilagServer::AntilagServer(INetworkHandler & network, const std::shared_ptr<CGameState> & gs)
-	: gameState(gs)
-{
-	antilagNetConnection = network.createAsyncConnection(*this);
-	antilagGameConnection = std::make_shared<GameConnection>(antilagNetConnection);
-}
-
-AntilagServer::~AntilagServer() = default;
-
-void AntilagServer::onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage)
-{
-	// should never be called
-	throw std::runtime_error("AntilagServer::onDisconnected called!");
-}
-
-void AntilagServer::onPacketReceived(const std::shared_ptr<INetworkConnection> & connection, const std::vector<std::byte> & message)
-{
-	std::scoped_lock interfaceLock(ENGINE->interfaceMutex);
-
-	auto basePack = antilagGameConnection->retrievePack(message);
-	auto * serverPack = dynamic_cast<CPackForServer*>(basePack.get());
-
-	AntilagReplyPredictionVisitor packVisitor;
-	serverPack->visit(packVisitor);
-	if (!packVisitor.canBeApplied())
-		return;
-
-	logGlobal->info("Predicting effects of pack '%s'", typeid(*serverPack).name());
-
-	AntilagReplyPrediction newPrediction;
-	newPrediction.requestID = serverPack->requestID;
-	newPrediction.senderID = serverPack->player;
-	predictedReplies.push_back(std::move(newPrediction));
-
-	try
-	{
-		CGameHandler gameHandler(*this, gameState);
-		gameHandler.handleReceivedPack(GameConnectionID::FIRST_CONNECTION, *serverPack);
-	}
-	catch (const AntilagRollbackNotSupportedException & )
-	{
-		return;
-	}
-}
-
-void AntilagServer::tryPredictReply(const CPackForServer & request)
-{
-	antilagGameConnection->sendPack(request);
-	logGlobal->info("Scheduled prediction of effects of pack '%s'", typeid(request).name());
-}
-
-bool AntilagServer::verifyReply(const CPackForClient & pack)
-{
-	logGlobal->info("Verifying reply: received pack '%s'", typeid(pack).name());
-
-	const auto * packageReceived = dynamic_cast<const PackageReceived*>(&pack);
-	const auto * packageApplied = dynamic_cast<const PackageApplied*>(&pack);
-
-	if (packageReceived)
-	{
-		assert(currentPackageID == invalidPackageID);
-
-		if (!predictedReplies.empty() && predictedReplies.front().requestID == packageReceived->requestID)
-		{
-			[[maybe_unused]] const auto & nextPrediction = predictedReplies.front();
-			assert(nextPrediction.senderID == packageReceived->player);
-			assert(nextPrediction.requestID == packageReceived->requestID);
-			currentPackageID = packageReceived->requestID;
-		}
-	}
-
-	if (currentPackageID == invalidPackageID)
-	{
-		// this is system package or reply to actions of another player
-		// TODO: consider reapplying all our predictions, in case if this event invalidated our prediction
-		return false;
-	}
-
-	ConnectionPackWriter packWriter;
-	BinarySerializer serializer(&packWriter);
-	serializer & &pack;
-
-	if (packWriter.buffer == predictedReplies.front().writtenPacks.front().buffer)
-	{
-		// Our prediction was sucessful - drop rollback information for this pack
-		predictedReplies.front().writtenPacks.erase(predictedReplies.front().writtenPacks.begin());
-
-		if (predictedReplies.front().writtenPacks.empty())
-		{
-			predictedReplies.erase(predictedReplies.begin());
-			currentPackageID = invalidPackageID;
-			return true;
-		}
-	}
-	else
-	{
-		// Prediction was incorrect - rollback everything that is left in this prediction and use real server packs
-		for (auto & prediction : boost::adaptors::reverse(predictedReplies.front().writtenPacks))
-		{
-			for (auto & pack : prediction.rollbackPacks)
-				GAME->server().client->handlePack(*pack);
-		}
-		predictedReplies.erase(predictedReplies.begin());
-		currentPackageID = invalidPackageID;
-		return false;
-	}
-
-	if (packageApplied)
-	{
-		assert(currentPackageID == packageApplied->requestID);
-		assert(!predictedReplies.empty());
-		assert(currentPackageID == predictedReplies.front().requestID);
-		assert(predictedReplies.front().writtenPacks.empty());
-		predictedReplies.erase(predictedReplies.begin());
-		currentPackageID = invalidPackageID;
-	}
-
-	return true;
-}
-
-void AntilagServer::setState(EServerState value)
-{
-	// no-op
-}
-
-EServerState AntilagServer::getState() const
-{
-	return EServerState::GAMEPLAY;
-}
-
-bool AntilagServer::isPlayerHost(const PlayerColor & color) const
-{
-	return false; // TODO?
-}
-
-bool AntilagServer::hasPlayerAt(PlayerColor player, GameConnectionID c) const
-{
-	return true; // TODO?
-}
-
-bool AntilagServer::hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const
-{
-	return false; // TODO?
-}
-
-void AntilagServer::applyPack(CPackForClient & pack)
-{
-	AntilagRollbackGeneratorVisitor visitor(*gameState);
-	pack.visit(visitor);
-	if (!visitor.canBeRolledBack())
-	{
-		logGlobal->info("Prediction not possible: pack '%s'", typeid(pack).name());
-		throw AntilagRollbackNotSupportedException(std::string("Prediction not possible ") + typeid(pack).name());
-	}
-
-	logGlobal->info("Prediction: pack '%s'", typeid(pack).name());
-
-	ConnectionPackWriter packWriter;
-	BinarySerializer serializer(&packWriter);
-	serializer & &pack;
-	packWriter.rollbackPacks = visitor.getRollbackPacks();
-	predictedReplies.back().writtenPacks.push_back(std::move(packWriter));
-
-	GAME->server().client->handlePack(pack);
-}
-
-void AntilagServer::sendPack(CPackForClient & pack, GameConnectionID connectionID)
-{
-	applyPack(pack);
-}

+ 9 - 2
client/CMakeLists.txt

@@ -88,6 +88,9 @@ set(vcmiclientcommon_SRCS
 	media/CSoundHandler.cpp
 	media/CVideoHandler.cpp
 
+	netlag/NetworkLagCompensator.cpp
+	netlag/PackRollbackGeneratorVisitor.cpp
+
 	render/AssetGenerator.cpp
 	render/CAnimation.cpp
 	render/CBitmapHandler.cpp
@@ -184,7 +187,6 @@ set(vcmiclientcommon_SRCS
 
 	xBRZ/xbrz.cpp
 
-	AntilagServer.cpp
 	ArtifactsUIController.cpp
 	GameEngine.cpp
 	GameInstance.cpp
@@ -301,6 +303,12 @@ set(vcmiclientcommon_HEADERS
 	media/ISoundPlayer.h
 	media/IVideoPlayer.h
 
+	netlag/NetworkLagCompensator.h
+	netlag/NetworkLagPredictedPack.h
+	netlag/NetworkLagPredictionTestVisitor.h
+	netlag/NetworkLagReplyPrediction.h
+	netlag/PackRollbackGeneratorVisitor.h
+
 	render/AssetGenerator.h
 	render/CAnimation.h
 	render/CBitmapHandler.h
@@ -408,7 +416,6 @@ set(vcmiclientcommon_HEADERS
 	xBRZ/xbrz.h
 	xBRZ/xbrz_tools.h
 
-	AntilagServer.h
 	ArtifactsUIController.h
 	CMT.h
 	CPlayerInterface.h

+ 22 - 9
client/CServerHandler.cpp

@@ -10,21 +10,24 @@
 #include "StdInc.h"
 #include "CServerHandler.h"
 
-#include "AntilagServer.h"
+#include "CPlayerInterface.h"
 #include "Client.h"
-#include "ServerRunner.h"
 #include "GameChatHandler.h"
-#include "CPlayerInterface.h"
 #include "GameEngine.h"
 #include "GameInstance.h"
-#include "gui/WindowHandler.h"
+#include "LobbyClientNetPackVisitors.h"
+#include "ServerRunner.h"
 
 #include "globalLobby/GlobalLobbyClient.h"
+
+#include "gui/WindowHandler.h"
+
 #include "lobby/CSelectionBase.h"
 #include "lobby/CLobbyScreen.h"
 #include "lobby/CBonusSelection.h"
-#include "windows/InfoWindows.h"
-#include "windows/GUIClasses.h"
+
+#include "netlag/NetworkLagCompensator.h"
+
 #include "media/CMusicHandler.h"
 #include "media/IVideoPlayer.h"
 
@@ -32,6 +35,9 @@
 #include "mainmenu/CPrologEpilogVideo.h"
 #include "mainmenu/CHighScoreScreen.h"
 
+#include "windows/InfoWindows.h"
+#include "windows/GUIClasses.h"
+
 #include "../lib/CConfigHandler.h"
 #include "../lib/GameLibrary.h"
 #include "../lib/texts/CGeneralTextHandler.h"
@@ -55,7 +61,6 @@
 #include <boost/uuid/uuid.hpp>
 #include <boost/uuid/uuid_io.hpp>
 #include <boost/uuid/uuid_generators.hpp>
-#include "LobbyClientNetPackVisitors.h"
 
 #include <vcmi/events/EventBus.h>
 #include <SDL_thread.h>
@@ -612,13 +617,21 @@ void CServerHandler::startMapAfterConnection(std::shared_ptr<CMapInfo> to)
 	mapToStart = to;
 }
 
+void CServerHandler::enableLagCompensation(bool on)
+{
+	if (on)
+		networkLagCompensator = std::make_unique<NetworkLagCompensator>(getNetworkHandler(),  client->gameStatePtr());
+	else
+		networkLagCompensator.reset();
+}
+
 void CServerHandler::startGameplay(std::shared_ptr<CGameState> gameState)
 {
 	if(GAME->mainmenu())
 		GAME->mainmenu()->disable();
 
-	//if (isGuest())
-	networkLagCompensator = std::make_unique<AntilagServer>(getNetworkHandler(), gameState);
+	if (isGuest())
+		networkLagCompensator = std::make_unique<NetworkLagCompensator>(getNetworkHandler(), gameState);
 
 	switch(si->mode)
 	{

+ 3 - 2
client/CServerHandler.h

@@ -33,7 +33,7 @@ class HighScoreParameter;
 
 VCMI_LIB_NAMESPACE_END
 
-class AntilagServer;
+class NetworkLagCompensator;
 class CClient;
 class CBaseForLobbyApply;
 class GlobalLobbyClient;
@@ -102,7 +102,7 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
 	std::unique_ptr<GlobalLobbyClient> lobbyClient;
 	std::unique_ptr<GameChatHandler> gameChat;
 	std::unique_ptr<IServerRunner> serverRunner;
-	std::unique_ptr<AntilagServer> networkLagCompensator;
+	std::unique_ptr<NetworkLagCompensator> networkLagCompensator;
 	std::shared_ptr<CMapInfo> mapToStart;
 	std::vector<std::string> localPlayerNames;
 
@@ -150,6 +150,7 @@ public:
 	void resetStateForLobby(EStartMode mode, ESelectionScreen screen, EServerMode serverMode, const std::vector<std::string> & playerNames);
 	void startLocalServerAndConnect(bool connectToLobby);
 	void connectToServer(const std::string & addr, const ui16 port);
+	void enableLagCompensation(bool on);
 
 	GameChatHandler & getGameChat();
 	GlobalLobbyClient & getGlobalLobby();

+ 1 - 0
client/Client.h

@@ -144,6 +144,7 @@ public:
 	vstd::CLoggerBase * logger() const override;
 	events::EventBus * eventBus() const override;
 
+	std::shared_ptr<CGameState> gameStatePtr() { return gamestate; }
 	CGameState & gameState() { return *gamestate; }
 	const CGameState & gameState() const { return *gamestate; }
 	IGameInfoCallback & gameInfo();

+ 26 - 0
client/ClientCommandManager.cpp

@@ -339,6 +339,29 @@ void ClientCommandManager::handleGetConfigCommand()
 	printCommandMessage("Extracted files can be found in " + outPath.string() + " directory\n");
 }
 
+void ClientCommandManager::handleAntilagCommand(std::istringstream& singleWordBuffer)
+{
+	std::string commandName;
+	singleWordBuffer >> commandName;
+
+	if (commandName == "on")
+	{
+		GAME->server().enableLagCompensation(true);
+		printCommandMessage("Network lag compensation is now enabled.\n");
+	}
+	else if (commandName == "off")
+	{
+		GAME->server().enableLagCompensation(true);
+		printCommandMessage("Network lag compensation is now disabled.\n");
+	}
+	else
+	{
+		printCommandMessage("Unexpected syntax. Supported forms:\n");
+		printCommandMessage("'antilag on'\n");
+		printCommandMessage("'antilag off'\n");
+	}
+}
+
 void ClientCommandManager::handleGetScriptsCommand()
 {
 #if SCRIPTING_ENABLED
@@ -596,6 +619,9 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 	else if(commandName == "setBattleAI")
 		handleSetBattleAICommand(singleWordBuffer);
 
+	else if(commandName == "antilag")
+		handleAntilagCommand(singleWordBuffer);
+
 	else if(commandName == "redraw")
 		handleRedrawCommand();
 

+ 3 - 0
client/ClientCommandManager.h

@@ -45,6 +45,9 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 	// Redraw the current screen
 	void handleRedrawCommand();
 
+	// Enable or disable network lag compensation
+	void handleAntilagCommand(std::istringstream& singleWordBuffer);
+
 	// Extracts all translateable game texts into Translation directory, separating files on per-mod basis
 	void handleTranslateGameCommand(bool onlyMissing);
 

+ 201 - 0
client/netlag/NetworkLagCompensator.cpp

@@ -0,0 +1,201 @@
+/*
+ * NetworkLagCompensator.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 "NetworkLagCompensator.h"
+
+#include "NetworkLagPredictionTestVisitor.h"
+#include "NetworkLagReplyPrediction.h"
+#include "PackRollbackGeneratorVisitor.h"
+
+#include "../CServerHandler.h"
+#include "../Client.h"
+#include "../GameEngine.h"
+#include "../GameInstance.h"
+
+#include "../../lib/serializer/BinarySerializer.h"
+#include "../../lib/serializer/GameConnection.h"
+#include "../../server/CGameHandler.h"
+
+class RollbackNotSupportedException : public std::runtime_error
+{
+public:
+	using std::runtime_error::runtime_error;
+};
+
+NetworkLagCompensator::NetworkLagCompensator(INetworkHandler & network, const std::shared_ptr<CGameState> & gs)
+	: gameState(gs)
+{
+	antilagNetConnection = network.createAsyncConnection(*this);
+	antilagGameConnection = std::make_shared<GameConnection>(antilagNetConnection);
+}
+
+NetworkLagCompensator::~NetworkLagCompensator() = default;
+
+void NetworkLagCompensator::onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage)
+{
+	// should never be called
+	throw std::runtime_error("AntilagServer::onDisconnected called!");
+}
+
+void NetworkLagCompensator::onPacketReceived(const std::shared_ptr<INetworkConnection> & connection, const std::vector<std::byte> & message)
+{
+	std::scoped_lock interfaceLock(ENGINE->interfaceMutex);
+
+	auto basePack = antilagGameConnection->retrievePack(message);
+	auto * serverPack = dynamic_cast<CPackForServer *>(basePack.get());
+
+	NetworkLagPredictionTestVisitor packVisitor;
+	serverPack->visit(packVisitor);
+	if(!packVisitor.canBeApplied())
+		return;
+
+	logGlobal->info("Predicting effects of pack '%s'", typeid(*serverPack).name());
+
+	NetworkLagReplyPrediction newPrediction;
+	newPrediction.requestID = serverPack->requestID;
+	newPrediction.senderID = serverPack->player;
+	predictedReplies.push_back(std::move(newPrediction));
+
+	try
+	{
+		CGameHandler gameHandler(*this, gameState);
+		gameHandler.handleReceivedPack(GameConnectionID::FIRST_CONNECTION, *serverPack);
+	}
+	catch(const RollbackNotSupportedException &)
+	{
+		return;
+	}
+}
+
+void NetworkLagCompensator::tryPredictReply(const CPackForServer & request)
+{
+	antilagGameConnection->sendPack(request);
+	logGlobal->info("Scheduled prediction of effects of pack '%s'", typeid(request).name());
+}
+
+bool NetworkLagCompensator::verifyReply(const CPackForClient & pack)
+{
+	logGlobal->info("Verifying reply: received pack '%s'", typeid(pack).name());
+
+	const auto * packageReceived = dynamic_cast<const PackageReceived *>(&pack);
+	const auto * packageApplied = dynamic_cast<const PackageApplied *>(&pack);
+
+	if(packageReceived)
+	{
+		assert(currentPackageID == invalidPackageID);
+
+		if(!predictedReplies.empty() && predictedReplies.front().requestID == packageReceived->requestID)
+		{
+			[[maybe_unused]] const auto & nextPrediction = predictedReplies.front();
+			assert(nextPrediction.senderID == packageReceived->player);
+			assert(nextPrediction.requestID == packageReceived->requestID);
+			currentPackageID = packageReceived->requestID;
+		}
+	}
+
+	if(currentPackageID == invalidPackageID)
+	{
+		// this is system package or reply to actions of another player
+		// TODO: consider reapplying all our predictions, in case if this event invalidated our prediction
+		return false;
+	}
+
+	NetworkLagPredictedPack packWriter;
+	BinarySerializer serializer(&packWriter);
+	serializer & &pack;
+
+	if(packWriter.predictedPackData == predictedReplies.front().writtenPacks.front().predictedPackData)
+	{
+		// Our prediction was sucessful - drop rollback information for this pack
+		predictedReplies.front().writtenPacks.erase(predictedReplies.front().writtenPacks.begin());
+
+		if(predictedReplies.front().writtenPacks.empty())
+		{
+			predictedReplies.erase(predictedReplies.begin());
+			currentPackageID = invalidPackageID;
+			return true;
+		}
+	}
+	else
+	{
+		// Prediction was incorrect - rollback everything that is left in this prediction and use real server packs
+		for(auto & prediction : boost::adaptors::reverse(predictedReplies.front().writtenPacks))
+		{
+			for(auto & pack : prediction.rollbackPacks)
+				GAME->server().client->handlePack(*pack);
+		}
+		predictedReplies.erase(predictedReplies.begin());
+		currentPackageID = invalidPackageID;
+		return false;
+	}
+
+	if(packageApplied)
+	{
+		assert(currentPackageID == packageApplied->requestID);
+		assert(!predictedReplies.empty());
+		assert(currentPackageID == predictedReplies.front().requestID);
+		assert(predictedReplies.front().writtenPacks.empty());
+		predictedReplies.erase(predictedReplies.begin());
+		currentPackageID = invalidPackageID;
+	}
+
+	return true;
+}
+
+void NetworkLagCompensator::setState(EServerState value)
+{
+	// no-op
+}
+
+EServerState NetworkLagCompensator::getState() const
+{
+	return EServerState::GAMEPLAY;
+}
+
+bool NetworkLagCompensator::isPlayerHost(const PlayerColor & color) const
+{
+	return false; // TODO?
+}
+
+bool NetworkLagCompensator::hasPlayerAt(PlayerColor player, GameConnectionID c) const
+{
+	return true; // TODO?
+}
+
+bool NetworkLagCompensator::hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const
+{
+	return false; // TODO?
+}
+
+void NetworkLagCompensator::applyPack(CPackForClient & pack)
+{
+	PackRollbackGeneratorVisitor visitor(*gameState);
+	pack.visit(visitor);
+	if(!visitor.canBeRolledBack())
+	{
+		logGlobal->info("Prediction not possible: pack '%s'", typeid(pack).name());
+		throw RollbackNotSupportedException(std::string("Prediction not possible ") + typeid(pack).name());
+	}
+
+	logGlobal->info("Prediction: pack '%s'", typeid(pack).name());
+
+	NetworkLagPredictedPack packWriter;
+	BinarySerializer serializer(&packWriter);
+	serializer & &pack;
+	packWriter.rollbackPacks = visitor.acquireRollbackPacks();
+	predictedReplies.back().writtenPacks.push_back(std::move(packWriter));
+
+	GAME->server().client->handlePack(pack);
+}
+
+void NetworkLagCompensator::sendPack(CPackForClient & pack, GameConnectionID connectionID)
+{
+	applyPack(pack);
+}

+ 52 - 0
client/netlag/NetworkLagCompensator.h

@@ -0,0 +1,52 @@
+/*
+ * NetworkLagCompensator.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/network/NetworkInterface.h"
+#include "../../server/IGameServer.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+struct CPackForServer;
+class CGameState;
+class GameConnection;
+VCMI_LIB_NAMESPACE_END
+
+class NetworkLagReplyPrediction;
+
+/// Fake server that is used by client to make a quick prediction on what real server would reply without waiting for network latency
+class NetworkLagCompensator final : public IGameServer, public INetworkConnectionListener, boost::noncopyable
+{
+	std::vector<NetworkLagReplyPrediction> predictedReplies;
+	std::shared_ptr<INetworkConnection> antilagNetConnection;
+	std::shared_ptr<GameConnection> antilagGameConnection;
+	std::shared_ptr<CGameState> gameState;
+
+	static constexpr uint32_t invalidPackageID = std::numeric_limits<uint32_t>::max();
+	uint32_t currentPackageID = invalidPackageID;
+
+	// IGameServer impl
+	void setState(EServerState value) override;
+	EServerState getState() const override;
+	bool isPlayerHost(const PlayerColor & color) const override;
+	bool hasPlayerAt(PlayerColor player, GameConnectionID connection) const override;
+	bool hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const override;
+	void applyPack(CPackForClient & pack) override;
+	void sendPack(CPackForClient & pack, GameConnectionID connectionID) override;
+
+	void onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage) override;
+	void onPacketReceived(const std::shared_ptr<INetworkConnection> & connection, const std::vector<std::byte> & message) override;
+
+public:
+	NetworkLagCompensator(INetworkHandler & network, const std::shared_ptr<CGameState> & gs);
+	~NetworkLagCompensator();
+
+	void tryPredictReply(const CPackForServer & request);
+	bool verifyReply(const CPackForClient & reply);
+};

+ 30 - 0
client/netlag/NetworkLagPredictedPack.h

@@ -0,0 +1,30 @@
+/*
+ * NetworkLagPredictedPack.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/networkPacks/NetPacksBase.h"
+#include "../../lib/serializer/CSerializer.h"
+
+/// Class that contains data for a single pack that client expects to receive from server
+class NetworkLagPredictedPack final : public IBinaryWriter
+{
+public:
+	/// Serialized data of predicted pack, for comparison with version from server
+	std::vector<std::byte> predictedPackData;
+
+	/// List of packs that must be applied on client in case of failed prediction
+	std::vector<std::unique_ptr<CPackForClient>> rollbackPacks;
+
+	int write(const std::byte * data, unsigned size) final
+	{
+		predictedPackData.insert(predictedPackData.end(), data, data + size);
+		return size;
+	}
+};

+ 67 - 0
client/netlag/NetworkLagPredictionTestVisitor.h

@@ -0,0 +1,67 @@
+/*
+ * AntilagServer.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/networkPacks/NetPackVisitor.h"
+
+/// Class that filters which client request to server can be predicted, at least partially
+class NetworkLagPredictionTestVisitor final : public ICPackVisitor
+{
+	bool canBeAppliedValue = false;
+
+	//void visitSaveGame(SaveGame & pack) override;
+	//void visitGamePause(GamePause & pack) override;
+	//void visitEndTurn(EndTurn & pack) override;
+	//void visitDismissHero(DismissHero & pack) override;
+	void visitMoveHero(MoveHero & pack) override
+	{
+		canBeAppliedValue = true;
+	}
+	//void visitCastleTeleportHero(CastleTeleportHero & pack) override;
+	void visitArrangeStacks(ArrangeStacks & pack) override
+	{
+		canBeAppliedValue = true;
+	}
+	//void visitBulkMoveArmy(BulkMoveArmy & pack) override;
+	//void visitBulkSplitStack(BulkSplitStack & pack) override;
+	//void visitBulkMergeStacks(BulkMergeStacks & pack) override;
+	//void visitBulkSplitAndRebalanceStack(BulkSplitAndRebalanceStack & pack) override;
+	//void visitDisbandCreature(DisbandCreature & pack) override;
+	//void visitBuildStructure(BuildStructure & pack) override;
+	//void visitSpellResearch(SpellResearch & pack) override;
+	//void visitVisitTownBuilding(VisitTownBuilding & pack) override;
+	//void visitRecruitCreatures(RecruitCreatures & pack) override;
+	//void visitUpgradeCreature(UpgradeCreature & pack) override;
+	//void visitGarrisonHeroSwap(GarrisonHeroSwap & pack) override;
+	//void visitExchangeArtifacts(ExchangeArtifacts & pack) override;
+	//void visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) override;
+	//void visitManageBackpackArtifacts(ManageBackpackArtifacts & pack) override;
+	//void visitManageEquippedArtifacts(ManageEquippedArtifacts & pack) override;
+	//void visitAssembleArtifacts(AssembleArtifacts & pack) override;
+	//void visitEraseArtifactByClient(EraseArtifactByClient & pack) override;
+	//void visitBuyArtifact(BuyArtifact & pack) override;
+	//void visitTradeOnMarketplace(TradeOnMarketplace & pack) override;
+	//void visitSetFormation(SetFormation & pack) override;
+	//void visitHireHero(HireHero & pack) override;
+	//void visitBuildBoat(BuildBoat & pack) override;
+	//void visitQueryReply(QueryReply & pack) override;
+	//void visitMakeAction(MakeAction & pack) override;
+	//void visitDigWithHero(DigWithHero & pack) override;
+	//void visitCastAdvSpell(CastAdvSpell & pack) override;
+	//void visitPlayerMessage(PlayerMessage & pack) override;
+	//void visitSaveLocalState(SaveLocalState & pack) override;
+
+public:
+	/// Returns true if game should try to predict results of pack on which this class was applied
+	bool canBeApplied() const
+	{
+		return canBeAppliedValue;
+	}
+};

+ 28 - 0
client/netlag/NetworkLagReplyPrediction.h

@@ -0,0 +1,28 @@
+/*
+ * NetworkLagReplyPrediction.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 "NetworkLagPredictedPack.h"
+
+#include "../../lib/constants/EntityIdentifiers.h"
+
+/// Class that contains data for a single pack that client sent to server
+class NetworkLagReplyPrediction
+{
+public:
+	/// Sender that sent this reply. Generally - this should be only local client color
+	PlayerColor senderID;
+
+	/// Unique request ID that was written into sent pack
+	uint32_t requestID;
+
+	/// One or more packs that client expects to receive from server as a reply
+	std::vector<NetworkLagPredictedPack> writtenPacks;
+};

+ 126 - 0
client/netlag/PackRollbackGeneratorVisitor.cpp

@@ -0,0 +1,126 @@
+/*
+ * PackRollbackGeneratorVisitor.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 "PackRollbackGeneratorVisitor.h"
+
+#include "../lib/gameState/CGameState.h"
+#include "../lib/mapObjects/CGHeroInstance.h"
+
+void PackRollbackGeneratorVisitor::visitPackageReceived(PackageReceived & pack)
+{
+	success = true;
+	// no-op rollback?
+}
+
+void PackRollbackGeneratorVisitor::visitPackageApplied(PackageApplied & pack)
+{
+	success = true;
+	// no-op rollback?
+}
+
+void PackRollbackGeneratorVisitor::visitPlayerBlocked(PlayerBlocked & pack)
+{
+	success = true;
+	// no-op rollback?
+}
+
+void PackRollbackGeneratorVisitor::visitSwapStacks(SwapStacks & pack)
+{
+	auto rollbackSwap = std::make_unique<SwapStacks>();
+
+	rollbackSwap->srcArmy = pack.dstArmy;
+	rollbackSwap->dstArmy = pack.srcArmy;
+	rollbackSwap->srcSlot = pack.dstSlot;
+	rollbackSwap->dstSlot = pack.srcSlot;
+
+	rollbackPacks.push_back(std::move(rollbackSwap));
+	success = true;
+}
+
+void PackRollbackGeneratorVisitor::visitRebalanceStacks(RebalanceStacks & pack)
+{
+	const auto * srcObject = gs.getObjInstance(pack.srcArmy);
+	const auto * dstObject = gs.getObjInstance(pack.dstArmy);
+
+	const auto * srcArmy = dynamic_cast<const CArmedInstance *>(srcObject);
+	const auto * dstArmy = dynamic_cast<const CArmedInstance *>(dstObject);
+
+	if (srcArmy->getStack(pack.srcSlot).getTotalExperience() != 0 ||
+	   dstArmy->getStack(pack.srcSlot).getTotalExperience() != 0 ||
+	   srcArmy->getStack(pack.srcSlot).getSlot(ArtifactPosition::CREATURE_SLOT)->artifactID.hasValue())
+	{
+		// TODO: rollback creature artifacts & stack experience
+		return;
+	}
+
+	auto rollbackRebalance = std::make_unique<RebalanceStacks>();
+	rollbackRebalance->srcArmy = pack.dstArmy;
+	rollbackRebalance->dstArmy = pack.srcArmy;
+	rollbackRebalance->srcSlot = pack.dstSlot;
+	rollbackRebalance->dstSlot = pack.srcSlot;
+	rollbackRebalance->count = pack.count;
+	rollbackPacks.push_back(std::move(rollbackRebalance));
+	success = true;
+}
+
+void PackRollbackGeneratorVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
+{
+	for(auto & subpack : pack.moves)
+		visitRebalanceStacks(subpack);
+
+	success = true;
+}
+
+void PackRollbackGeneratorVisitor::visitHeroVisitCastle(HeroVisitCastle & pack)
+{
+	auto rollbackVisit = std::make_unique<HeroVisitCastle>();
+	rollbackVisit->startVisit = !pack.startVisit;
+	rollbackVisit->tid = pack.tid;
+	rollbackVisit->hid = pack.hid;
+
+	rollbackPacks.push_back(std::move(rollbackVisit));
+
+	success = true;
+}
+
+void PackRollbackGeneratorVisitor::visitTryMoveHero(TryMoveHero & pack)
+{
+	auto rollbackMove = std::make_unique<TryMoveHero>();
+	auto rollbackFow = std::make_unique<FoWChange>();
+	const auto * movedHero = gs.getHero(pack.id);
+
+	rollbackMove->id = pack.id;
+	rollbackMove->movePoints = movedHero->movementPointsRemaining();
+	rollbackMove->result = pack.result;
+	if(pack.result == TryMoveHero::EMBARK)
+		rollbackMove->result = TryMoveHero::DISEMBARK;
+	if(pack.result == TryMoveHero::DISEMBARK)
+		rollbackMove->result = TryMoveHero::EMBARK;
+	rollbackMove->start = pack.end;
+	rollbackMove->end = pack.start;
+
+	rollbackFow->mode = ETileVisibility::HIDDEN;
+	rollbackFow->player = movedHero->getOwner();
+	rollbackFow->tiles = pack.fowRevealed;
+
+	rollbackPacks.push_back(std::move(rollbackMove));
+	rollbackPacks.push_back(std::move(rollbackFow));
+	success = true;
+}
+
+bool PackRollbackGeneratorVisitor::canBeRolledBack() const
+{
+	return success;
+}
+
+std::vector<std::unique_ptr<CPackForClient>> PackRollbackGeneratorVisitor::acquireRollbackPacks()
+{
+	return std::move(rollbackPacks);
+}

+ 14 - 110
client/AntilagServer.h → client/netlag/PackRollbackGeneratorVisitor.h

@@ -1,5 +1,5 @@
 /*
- * AntilagServer.h, part of VCMI engine
+ * PackRollbackGeneratorVisitor.h, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *
@@ -9,83 +9,13 @@
  */
 #pragma once
 
-#include "../lib/networkPacks/NetPackVisitor.h"
-#include "../server/IGameServer.h"
-#include "../lib/network/NetworkInterface.h"
-#include "../lib/serializer/CSerializer.h"
-#include "../lib/serializer/BinarySerializer.h"
+#include "../../lib/networkPacks/NetPackVisitor.h"
 
-VCMI_LIB_NAMESPACE_BEGIN
-struct CPackForServer;
-class GameConnection;
-VCMI_LIB_NAMESPACE_END
-
-class CGameHandler;
-
-class ConnectionPackWriter final : public IBinaryWriter
-{
-public:
-	std::vector<std::byte> buffer;
-	std::vector<std::unique_ptr<CPackForClient>> rollbackPacks;
-
-	int write(const std::byte * data, unsigned size) final;
-};
-
-class AntilagReplyPrediction
-{
-public:
-	PlayerColor senderID;
-	uint32_t requestID;
-	std::vector<ConnectionPackWriter> writtenPacks;
-};
-
-class AntilagReplyPredictionVisitor final : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor)
-{
-	bool canBeAppliedValue = false;
-
-	//void visitSaveGame(SaveGame & pack) override;
-	//void visitGamePause(GamePause & pack) override;
-	//void visitEndTurn(EndTurn & pack) override;
-	//void visitDismissHero(DismissHero & pack) override;
-	void visitMoveHero(MoveHero & pack) override;
-	//void visitCastleTeleportHero(CastleTeleportHero & pack) override;
-	void visitArrangeStacks(ArrangeStacks & pack) override;
-	//void visitBulkMoveArmy(BulkMoveArmy & pack) override;
-	//void visitBulkSplitStack(BulkSplitStack & pack) override;
-	//void visitBulkMergeStacks(BulkMergeStacks & pack) override;
-	//void visitBulkSplitAndRebalanceStack(BulkSplitAndRebalanceStack & pack) override;
-	//void visitDisbandCreature(DisbandCreature & pack) override;
-	//void visitBuildStructure(BuildStructure & pack) override;
-	//void visitSpellResearch(SpellResearch & pack) override;
-	//void visitVisitTownBuilding(VisitTownBuilding & pack) override;
-	//void visitRecruitCreatures(RecruitCreatures & pack) override;
-	//void visitUpgradeCreature(UpgradeCreature & pack) override;
-	//void visitGarrisonHeroSwap(GarrisonHeroSwap & pack) override;
-	//void visitExchangeArtifacts(ExchangeArtifacts & pack) override;
-	//void visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) override;
-	//void visitManageBackpackArtifacts(ManageBackpackArtifacts & pack) override;
-	//void visitManageEquippedArtifacts(ManageEquippedArtifacts & pack) override;
-	//void visitAssembleArtifacts(AssembleArtifacts & pack) override;
-	//void visitEraseArtifactByClient(EraseArtifactByClient & pack) override;
-	//void visitBuyArtifact(BuyArtifact & pack) override;
-	//void visitTradeOnMarketplace(TradeOnMarketplace & pack) override;
-	//void visitSetFormation(SetFormation & pack) override;
-	//void visitHireHero(HireHero & pack) override;
-	//void visitBuildBoat(BuildBoat & pack) override;
-	//void visitQueryReply(QueryReply & pack) override;
-	//void visitMakeAction(MakeAction & pack) override;
-	//void visitDigWithHero(DigWithHero & pack) override;
-	//void visitCastAdvSpell(CastAdvSpell & pack) override;
-	//void visitPlayerMessage(PlayerMessage & pack) override;
-	//void visitSaveLocalState(SaveLocalState & pack) override;
-
-public:
-	AntilagReplyPredictionVisitor();
-
-	bool canBeApplied() const;
-};
-
-class AntilagRollbackGeneratorVisitor final : public ICPackVisitor
+/// Class that generates data for rollback
+/// on success, canBeRolledBack() method will return true
+/// and rollbackPacks will contain list of packs that can be applied to rollback provided pack
+/// Note that it is legal for rollbackPacks list to be empty for some trivial packs
+class PackRollbackGeneratorVisitor final : public ICPackVisitor
 {
 private:
 	const CGameState & gs;
@@ -173,41 +103,15 @@ private:
 	//void visitTurnTimeUpdate(TurnTimeUpdate & pack) override;
 
 public:
-	AntilagRollbackGeneratorVisitor(const CGameState & gs)
+	PackRollbackGeneratorVisitor(const CGameState & gs)
 		: gs(gs)
-	{}
+	{
+	}
 
+	/// Returns true if tested pack can be rolled back
 	bool canBeRolledBack() const;
-	std::vector<std::unique_ptr<CPackForClient>> getRollbackPacks();
-};
-
-// Fake server that is used by client to make a quick prediction on what real server would reply without waiting for network latency
-class AntilagServer final : public IGameServer, public INetworkConnectionListener, boost::noncopyable
-{
-	std::vector<AntilagReplyPrediction> predictedReplies;
-	std::shared_ptr<INetworkConnection> antilagNetConnection;
-	std::shared_ptr<GameConnection> antilagGameConnection;
-	std::shared_ptr<CGameState> gameState;
-
-	static constexpr uint32_t invalidPackageID = std::numeric_limits<uint32_t>::max();
-	uint32_t currentPackageID = invalidPackageID;
-
-	// IGameServer impl
-	void setState(EServerState value) override;
-	EServerState getState() const override;
-	bool isPlayerHost(const PlayerColor & color) const override;
-	bool hasPlayerAt(PlayerColor player, GameConnectionID connection) const override;
-	bool hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const override;
-	void applyPack(CPackForClient & pack) override;
-	void sendPack(CPackForClient & pack, GameConnectionID connectionID) override;
-
-	void onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage) override;
-	void onPacketReceived(const std::shared_ptr<INetworkConnection> & connection, const std::vector<std::byte> & message) override;
-
-public:
-	AntilagServer(INetworkHandler & network, const std::shared_ptr<CGameState> & gs);
-	~AntilagServer();
 
-	void tryPredictReply(const CPackForServer & request);
-	bool verifyReply(const CPackForClient & reply);
+	/// Acquires list of packs that can be used to rollback tested pack
+	/// (!) non-reentrable
+	std::vector<std::unique_ptr<CPackForClient>> acquireRollbackPacks();
 };