Browse Source

Merge pull request #6348 from mrhaandi/quick-save-load

QuickSave / QuickLoad
Ivan Savenko 2 tuần trước cách đây
mục cha
commit
7f3a2b8311

+ 1 - 0
Mods/vcmi/Content/config/chinese.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "移动到此处总计花费{%POINTS}移动力。移动完成后还会剩下{%REMAINING}点。",
 	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(移动点数: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "抱歉,重放对手行动功能目前暂未实现!",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "您確定要載入快速存檔嗎?",
 
 	"vcmi.adventureMap.dwelling2" : "{%s}\n\n你想招募%s还是%s?",
 	"vcmi.adventureMap.dwelling3" : "{%s}\n\n你想招募%s、%s还是%s?",

+ 1 - 0
Mods/vcmi/Content/config/czech.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Přesun sem tě bude stát {%POINTS} bodů. Po přesunu ti zbyde {%REMAINING} bodů.",
 	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Body pohybu: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Omlouváme se, přehrání tahu soupeře ještě není implementováno!",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Jste si jisti, že chcete načíst rychlé uložení?",
 
 	"vcmi.adventureMap.dwelling2" : "{%s}\n\nChceš najmout jednotku %s nebo %s?",
 	"vcmi.adventureMap.dwelling3" : "{%s}\n\nChceš najmout jednotku %s, %s nebo %s?",

+ 8 - 0
Mods/vcmi/Content/config/english.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Moving here will cost {%POINTS} points. {%REMAINING} points will remain after moving.",
 	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Movement points: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sorry, replay opponent turn is not implemented yet!",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Are you sure you want to load the quicksave?",
 
 	"vcmi.adventureMap.dwelling2" : "{%s}\n\nWould you like to recruit %s or %s?",
 	"vcmi.adventureMap.dwelling3" : "{%s}\n\nWould you like to recruit %s, %s, or %s?",
@@ -343,6 +344,8 @@
 	"vcmi.keyBindings.keyBinding.adventureReplayTurn": "Adventure replay turn",
 	"vcmi.keyBindings.keyBinding.adventureRestartGame": "Adventure restart game",
 	"vcmi.keyBindings.keyBinding.adventureSaveGame": "Adventure save game",
+	"vcmi.keyBindings.keyBinding.adventureQuickSave": "Adventure quick save",
+	"vcmi.keyBindings.keyBinding.adventureQuickLoad": "Adventure quick load",
 	"vcmi.keyBindings.keyBinding.adventureSetHeroAsleep": "Adventure set hero asleep",
 	"vcmi.keyBindings.keyBinding.adventureSetHeroAwake": "Adventure set hero awake",
 	"vcmi.keyBindings.keyBinding.adventureThievesGuild": "Adventure thieves guild",
@@ -706,6 +709,11 @@
 	"vcmi.adventureMap.revisitObject.hover" : "Revisit Object",
 	"vcmi.adventureMap.revisitObject.help" : "{Revisit Object}\n\nIf a hero currently stands on a Map Object, he can revisit the location.",
 
+	"vcmi.adventureMap.quickLoad.hover" : "Quick Load",
+	"vcmi.adventureMap.quickLoad.help" : "{Quick Load}\n\nLoads the game state from a quick save slot.",
+	"vcmi.adventureMap.quickSave.hover" : "Quick Save",
+	"vcmi.adventureMap.quickSave.help" : "{Quick Save}\n\nSaves the current game state to a quick save slot.",
+
 	"vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to start battle immediately",
 	"vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).",
 	"vcmi.battleWindow.damageEstimation.meleeKills" : "Attack %CREATURE (%DAMAGE, %KILLS).",

+ 1 - 0
Mods/vcmi/Content/config/german.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Eine Bewegung hierher kostet {%POINTS} Punkte. Nach der Bewegung bleiben {%REMAINING} Punkte übrig.",
 	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Bewegungspunkte: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Das Wiederholen des gegnerischen Zuges ist aktuell noch nicht implementiert!",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Seid Ihr sicher, dass Ihr den Schnellspielstand laden wollt?",
 
 	"vcmi.adventureMap.dwelling2" : "{%s}\r\n\r\nWürdet Ihr gerne %s oder %s rekrutieren?",
 	"vcmi.adventureMap.dwelling3" : "{%s}\r\n\r\nWürdet Ihr gerne %s, %s oder %s rekrutieren?",

+ 1 - 0
Mods/vcmi/Content/config/hungarian.json

@@ -25,6 +25,7 @@
 	"vcmi.adventureMap.playerAttacked"                   : "A játékost megtámadták: %s",
 	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Mozgáspontok: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sajnáljuk, az ellenfél körének visszajátszása még nincs megvalósítva!",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Biztosan betöltöd a gyorsmentést?",
 
 	"vcmi.bonusSource.artifact" : "Műtárgy",
 	"vcmi.bonusSource.creature" : "Képesség",

+ 1 - 0
Mods/vcmi/Content/config/italian.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Punti movimento - Costo: %POINTS punti, Punti rimanenti: %REMAINING",
 	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Punti movimento: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Spiacente, la riproduzione del turno avversario non è ancora implementata!",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Sei sicuro di voler caricare il salvataggio rapido?",
 
 	"vcmi.bonusSource.artifact" : "Artefatto",
 	"vcmi.bonusSource.creature" : "Abilità",

+ 1 - 0
Mods/vcmi/Content/config/polish.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Ruch tutaj będzie kosztował {%POINTS} punktów. {%REMAINING} punktów zostanie po ruchu.",
 	"vcmi.adventureMap.movementPointsHeroInfo" : "(Punkty ruchu: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Wybacz, powtórka ruchu wroga nie została jeszcze zaimplementowana!",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Czy na pewno chcesz załadować szybki zapis?",
 
 	"vcmi.adventureMap.dwelling2" : "{%s}\n\nCzy chciałbyś zrekrutować %s lub %s?",
 	"vcmi.adventureMap.dwelling3" : "{%s}\n\nCzy chciałbyś zrekrutować %s, %s, lub %s?",

+ 1 - 0
Mods/vcmi/Content/config/portuguese.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Mover para cá custará {%POINTS} pontos. {%REMAINING} pontos restantes após mover.",
 	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Pontos de movimento: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Desculpe, a repetição do turno do oponente ainda não foi implementada!",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Tem a certeza de que pretende carregar o jogo guardado rapidamente?",
 
 	"vcmi.adventureMap.dwelling2" : "{%s}\n\nVocê gostaria de recrutar %s ou %s?",
 	"vcmi.adventureMap.dwelling3" : "{%s}\n\nVocê gostaria de recrutar %s, %s ou %s?",

+ 2 - 1
Mods/vcmi/Content/config/romanian.json

@@ -27,7 +27,8 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Mutarea aici va costa {%POINTS} puncte. Vor rămâne {%REMAINING} puncte după mutare.",
 	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Puncte de mișcare: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Ne pare rău, redarea turei inamicului nu este încă implementată!",
-	
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Sigur vrei să încarci salvarea rapidă?",
+
 	"vcmi.adventureMap.dwelling2" : "{%s}\n\nDorești să recrutezi %s sau %s?",
 	"vcmi.adventureMap.dwelling3" : "{%s}\n\nDorești să recrutezi %s, %s sau %s?",
 	

+ 2 - 0
Mods/vcmi/Content/config/russian.json

@@ -40,6 +40,8 @@
 	"vcmi.battle.action.return" : "Использовать атаку ближнего боя и вернуться",
 	"vcmi.battle.action.genie" : "Наложить случайное полезное заклинание",
 	
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Вы уверены, что хотите загрузить быстрый сохраненный файл?",
+
 	"vcmi.bonusSource.artifact" : "Артефакт",
 	"vcmi.bonusSource.creature" : "Умение",
 	"vcmi.bonusSource.spell" : "Заклинание",

+ 1 - 0
Mods/vcmi/Content/config/spanish.json

@@ -19,6 +19,7 @@
 	"vcmi.adventureMap.spellUnknownProblem"    : "Problema desconocido con este hechizo, no hay más información disponible.",
 	"vcmi.adventureMap.playerAttacked"         : "El jugador ha sido atacado: %s",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Disculpe, la repetición del turno del oponente aún no está implementada.",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "¿Estás segura de que quieres cargar el quicksave?",
 
 	"vcmi.capitalColors.0" : "Rojo",
 	"vcmi.capitalColors.1" : "Azul",

+ 1 - 0
Mods/vcmi/Content/config/swedish.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns"          : "Att förflytta sig hit kostar {%POINTS} färdpoäng. Kvar efter flytt: {%REMAINING} färdpoäng.",
 	"vcmi.adventureMap.movementPointsHeroInfo"          : "(Färdpoäng: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented": "Tyvärr, att spela om motståndarens tur är inte implementerat ännu!",
+	"vcmi.adventureMap.confirmQuickLoadGame"            : "Är du säker på att du vill ladda snabbsparningen?",
 
 	"vcmi.adventureMap.dwelling2": "{%s}\n\nVill du anlita enheten %s eller %s?",
 	"vcmi.adventureMap.dwelling3": "{%s}\n\nVill du anlita enheten %s, %s eller %s?",

+ 1 - 0
Mods/vcmi/Content/config/ukrainian.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Переміщення сюди коштуватиме {%TOTAL} очок. Після переміщення залишиться {%REMAINING} очок.",
 	"vcmi.adventureMap.movementPointsHeroInfo" : "(Очки руху: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Вибачте, функція повтору ходу суперника ще не реалізована!",
+	"vcmi.adventureMap.confirmQuickLoadGame"             : "Ви впевнені, що хочете завантажити швидке збереження?",
 
 	"vcmi.artifact.charges" : "Заряди",
 	

+ 1 - 0
Mods/vcmi/Content/config/vietnamese.json

@@ -27,6 +27,7 @@
 	"vcmi.adventureMap.moveCostDetailsNoTurns": "Đi đến đây sẽ tốn {%POINTS} điểm di chuyển. Còn lại {%REMAINING} điểm sau khi di chuyển.",
 	"vcmi.adventureMap.movementPointsHeroInfo": "(Điểm di chuyển: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented": "Xin lỗi, lượt chơi lại của đối thủ vẫn chưa được triển khai!",
+	"vcmi.adventureMap.confirmQuickLoadGame"            : "Bạn có chắc chắn muốn tải quicksave không?",
 
 	"vcmi.adventureMap.dwelling2" : "{%s}\n\nBạn có muốn chiêu mộ %s hoặc %s?",
 	"vcmi.adventureMap.dwelling3" : "{%s}\n\nBạn có muốn chiêu mộ %s, %s, hoặc %s?",

+ 33 - 0
client/CPlayerInterface.cpp

@@ -116,6 +116,8 @@
 
 #include "../lib/texts/TextOperations.h"
 
+#include "../lib/filesystem/Filesystem.h"
+
 #include <boost/lexical_cast.hpp>
 
 // The macro below is used to mark functions that are called by client when game state changes.
@@ -1800,6 +1802,37 @@ void CPlayerInterface::proposeLoadingGame()
 	);
 }
 
+void CPlayerInterface::quickSaveGame()
+{
+	std::string path = "Saves/Quicksave";
+	// notify player about saving
+	GAME->server().getGameChat().sendMessageGameplay("Saving game to " + path);
+	GAME->interface()->cb->save(path);
+}
+
+void CPlayerInterface::proposeQuickLoadingGame()
+{
+	std::string path = "Saves/Quicksave";
+
+	if(!CResourceHandler::get("local")->existsResource(ResourcePath(path, EResType::SAVEGAME)))
+	{
+		logGlobal->error("No quicksave file found at %s", path);
+		return;
+	}
+	auto error = GAME->server().canQuickLoadGame(path);
+	if (error)
+	{
+		logGlobal->error("Cannot quick load game at %s: %s", path, *error);
+		return;
+	}
+	auto onYes = [path]() -> void
+	{
+		GAME->server().quickLoadGame(path);
+	};
+
+	GAME->interface()->showYesNoDialog(LIBRARY->generaltexth->translate("vcmi.adventureMap.confirmQuickLoadGame"), onYes, nullptr);
+}
+
 bool CPlayerInterface::capturedAllEvents()
 {
 	if(movementController->isHeroMoving())

+ 3 - 0
client/CPlayerInterface.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "ArtifactsUIController.h"
+#include "GameChatHandler.h"
 
 #include "../lib/callback/CGameInterface.h"
 #include "../lib/gameState/GameStatistics.h"
@@ -200,6 +201,8 @@ public: // public interface for use by client via GAME->interface() access
 	void tryDigging(const CGHeroInstance *h);
 	void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard;
 	void proposeLoadingGame();
+	void proposeQuickLoadingGame();
+	void quickSaveGame();
 	void performAutosave();
 	void gamePause(bool pause);
 	void endNetwork();

+ 44 - 0
client/CServerHandler.cpp

@@ -692,6 +692,50 @@ void CServerHandler::endGameplay()
 	}
 }
 
+std::optional<std::string> CServerHandler::canQuickLoadGame(const std::string & path) const
+{
+	auto mapInfo = std::make_shared<CMapInfo>();
+	mapInfo->saveInit(ResourcePath(path, EResType::SAVEGAME));
+
+	// initial start info from quick load slot
+	const auto * startInfo1 = mapInfo->scenarioOptionsOfSave.get();
+	// initial start info from game state (not current start info)
+	const auto * startInfo2 = client->gameState().getInitialStartInfo();
+
+	if (!startInfo1)
+		return "Missing quick load start info.";
+	if (!startInfo2)
+		return "Missing server start info.";
+	if (startInfo1->startTime != startInfo2->startTime)
+		return "Different initial start time.";
+	if (startInfo1->mapname != startInfo2->mapname)
+		return "Different map name.";
+	if (startInfo1->difficulty != startInfo2->difficulty)
+		return "Different difficulty.";
+	const auto & playerInfos1 = startInfo1->playerInfos;
+	const auto & playerInfos2 = startInfo2->playerInfos;
+	if (playerInfos1.size() != playerInfos2.size())
+		return "Different number of players.";
+	if (!std::equal(playerInfos1.begin(), playerInfos1.end(), playerInfos2.begin(), playerInfos2.end(),
+		[](const auto& p1, const auto& p2) { return p1.first == p2.first && p1.second.connectedPlayerIDs == p2.second.connectedPlayerIDs; }))
+		return "Different players.";
+	return std::nullopt;
+}
+
+void CServerHandler::quickLoadGame(const std::string & path)
+{
+	if(!settings["session"]["headless"].Bool())
+	{
+		if(si->campState && !si->campState->getLoadingBackground().empty())
+			ENGINE->windows().createAndPushWindow<CLoadingScreen>(si->campState->getLoadingBackground());
+		else
+			ENGINE->windows().createAndPushWindow<CLoadingScreen>();
+	}
+	LobbyQuickLoadGame pack;
+	pack.saveFilePath = path;
+	sendLobbyPack(pack);
+}
+
 void CServerHandler::restartGameplay()
 {
 	client->finishGameplay();

+ 4 - 0
client/CServerHandler.h

@@ -13,6 +13,8 @@
 
 #include "../lib/network/NetworkInterface.h"
 #include "../lib/StartInfo.h"
+#include "../lib/mapping/CMapInfo.h"
+#include "../lib/mapping/CMapHeader.h"
 #include "../lib/gameState/GameStatistics.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -208,6 +210,8 @@ public:
 	void debugStartTest(std::string filename, bool save = false);
 
 	void startGameplay(std::shared_ptr<CGameState> gameState);
+	std::optional<std::string> canQuickLoadGame(const std::string & path) const; // returns reason why not compatible, or nullopt if can
+	void quickLoadGame(const std::string & path);
 	void showHighScoresAndEndGameplay(PlayerColor player, bool victory, const StatisticDataSet & statistic);
 	void endNetwork();
 	void endGameplay();

+ 1 - 0
client/LobbyClientNetPackVisitors.h

@@ -32,6 +32,7 @@ public:
 
 	bool getResult() const { return result; }
 
+	void visitLobbyQuickLoadGame(LobbyQuickLoadGame & pack) override;
 	void visitLobbyClientConnected(LobbyClientConnected & pack) override;
 	void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override;
 	void visitLobbyRestartGame(LobbyRestartGame & pack) override;

+ 12 - 0
client/NetPacksLobbyClient.cpp

@@ -41,6 +41,11 @@
 #include "../lib/serializer/GameConnection.h"
 #include "../lib/texts/CGeneralTextHandler.h"
 
+void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyQuickLoadGame(LobbyQuickLoadGame & pack)
+{
+	assert(handler.getState() == EClientState::GAMEPLAY);
+}
+
 void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack)
 {
 	result = false;
@@ -155,6 +160,13 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyPrepareStartGame(LobbyPrepareS
 
 void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack)
 {
+	// Handle mid-game reload
+	if (handler.getState() == EClientState::GAMEPLAY) {
+			handler.client->finishGameplay();
+			handler.client->endGame();
+			handler.client.reset();
+	}
+
 	handler.setState(EClientState::STARTING);
 	if(handler.si->mode != EStartMode::LOAD_GAME)
 	{

+ 27 - 0
client/adventureMap/AdventureMapShortcuts.cpp

@@ -97,6 +97,8 @@ std::vector<AdventureMapShortcutState> AdventureMapShortcuts::getShortcuts()
 		{ EShortcut::ADVENTURE_SAVE_GAME,        optionInMapView(),      [this]() { this->saveGame(); } },
 		{ EShortcut::ADVENTURE_NEW_GAME,         optionInMapView(),      [this]() { this->newGame(); } },
 		{ EShortcut::ADVENTURE_LOAD_GAME,        optionInMapView(),      [this]() { this->loadGame(); } },
+		{ EShortcut::ADVENTURE_QUICK_SAVE,       optionIsLocal(),        [this]() { this->quickSaveGame(); } },
+		{ EShortcut::ADVENTURE_QUICK_LOAD,       optionIsLocal(),        [this]() { this->quickLoadGame(); } },
 		{ EShortcut::ADVENTURE_RESTART_GAME,     optionInMapView(),      [this]() { this->restartGame(); } },
 		{ EShortcut::ADVENTURE_DIG_GRAIL,        optionHeroDig(),        [this]() { this->digGrail(); } },
 		{ EShortcut::ADVENTURE_VIEW_PUZZLE,      optionSidePanelActive(),[this]() { this->viewPuzzleMap(); } },
@@ -374,6 +376,16 @@ void AdventureMapShortcuts::loadGame()
 	GAME->interface()->proposeLoadingGame();
 }
 
+void AdventureMapShortcuts::quickSaveGame()
+{
+	GAME->interface()->quickSaveGame();
+}
+
+void AdventureMapShortcuts::quickLoadGame()
+{
+	GAME->interface()->proposeQuickLoadingGame();
+}
+
 void AdventureMapShortcuts::digGrail()
 {
 	const CGHeroInstance *h = GAME->interface()->localState->getCurrentHero();
@@ -691,3 +703,18 @@ bool AdventureMapShortcuts::optionViewStatistic()
 	auto day = GAME->interface()->cb->getDate(Date::DAY);
 	return optionInMapView() && day > 1;
 }
+
+bool AdventureMapShortcuts::optionIsLocal()
+{
+	if (!optionInMapView() || !GAME->server().isHost() || !(GAME->server().serverMode == EServerMode::LOCAL))
+		return false;
+	
+	//exclude local multiplayer games (hot seat is ok)
+	auto hostClientId = GAME->server().hostClientId;
+	for(const auto& playerName : GAME->server().playerNames)
+	{
+		if(playerName.second.connection != hostClientId)
+			return false;
+	}
+	return true;
+}

+ 3 - 0
client/adventureMap/AdventureMapShortcuts.h

@@ -67,6 +67,8 @@ class AdventureMapShortcuts
 	void quitGame();
 	void saveGame();
 	void loadGame();
+	void quickSaveGame();
+	void quickLoadGame();
 	void digGrail();
 	void viewPuzzleMap();
 	void restartGame();
@@ -105,6 +107,7 @@ public:
 	bool optionHeroBoat(EPathfindingLayer layer);
 	bool optionHeroDig();
 	bool optionViewStatistic();
+	bool optionIsLocal();
 
 	void setState(EAdventureState newState);
 	EAdventureState getState() const;

+ 4 - 0
client/battle/BattleWindow.cpp

@@ -106,6 +106,10 @@ BattleWindow::BattleWindow(BattleInterface & Owner)
 	addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();});
 	addShortcut(EShortcut::BATTLE_USE_CREATURE_SPELL, [this](){ this->owner.actionsController->enterCreatureCastingMode(); });
 	addShortcut(EShortcut::GLOBAL_CANCEL, [this](){ this->owner.actionsController->endCastingSpell(); });
+	addShortcut(EShortcut::ADVENTURE_QUICK_LOAD, [this](){
+		//allow quick load only on player turn while no animations are ongoing
+		if (!this->owner.hasAnimations() && this->owner.stacksController->getActiveStack())
+			GAME->interface()->proposeQuickLoadingGame(); });
 
 	build(config);
 	

+ 2 - 0
client/gui/Shortcut.h

@@ -164,6 +164,8 @@ enum class EShortcut
 	ADVENTURE_END_TURN,
 	ADVENTURE_LOAD_GAME,
 	ADVENTURE_SAVE_GAME,
+	ADVENTURE_QUICK_SAVE,
+	ADVENTURE_QUICK_LOAD,
 	ADVENTURE_NEW_GAME,
 	ADVENTURE_RESTART_GAME,
 	ADVENTURE_TO_MAIN_MENU,

+ 2 - 0
client/gui/ShortcutHandler.cpp

@@ -167,6 +167,8 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"adventureEndTurn",         EShortcut::ADVENTURE_END_TURN        },
 		{"adventureLoadGame",        EShortcut::ADVENTURE_LOAD_GAME       },
 		{"adventureSaveGame",        EShortcut::ADVENTURE_SAVE_GAME       },
+		{"adventureQuickSave",       EShortcut::ADVENTURE_QUICK_SAVE      },
+		{"adventureQuickLoad",       EShortcut::ADVENTURE_QUICK_LOAD      },
 		{"adventureRestartGame",     EShortcut::ADVENTURE_RESTART_GAME    },
 		{"adventureMainMenu",        EShortcut::ADVENTURE_TO_MAIN_MENU    },
 		{"adventureQuitGame",        EShortcut::ADVENTURE_QUIT_GAME       },

+ 2 - 0
config/keyBindingsConfig.json

@@ -32,6 +32,8 @@
 		"adventureReplayTurn":      [], // NOTE: functionality not implemented
 		"adventureRestartGame":     "R",
 		"adventureSaveGame":        "S",
+		"adventureQuickSave":       ["F8"],
+		"adventureQuickLoad":       ["F9"],
 		"adventureSetHeroAsleep":   "Z",
 		"adventureSetHeroAwake":    "W",
 		"adventureThievesGuild":    "G",

+ 1 - 0
lib/networkPacks/NetPackVisitor.h

@@ -158,6 +158,7 @@ public:
 	virtual void visitPlayerMessage(PlayerMessage & pack) {}
 	virtual void visitPlayerMessageClient(PlayerMessageClient & pack) {}
 	virtual void visitCenterView(CenterView & pack) {}
+	virtual	void visitLobbyQuickLoadGame(LobbyQuickLoadGame & pack) {}
 	virtual void visitLobbyClientConnected(LobbyClientConnected & pack) {}
 	virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) {}
 	virtual void visitLobbyChatMessage(LobbyChatMessage & pack) {}

+ 5 - 0
lib/networkPacks/NetPacksLib.cpp

@@ -753,6 +753,11 @@ void LobbyPrepareStartGame::visitTyped(ICPackVisitor & visitor)
 	visitor.visitLobbyPrepareStartGame(*this);
 }
 
+void LobbyQuickLoadGame::visitTyped(ICPackVisitor & visitor)
+{
+	visitor.visitLobbyQuickLoadGame(*this);
+}
+
 void LobbyChangeHost::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitLobbyChangeHost(*this);

+ 12 - 0
lib/networkPacks/PacksForLobby.h

@@ -151,6 +151,18 @@ struct DLL_LINKAGE LobbyStartGame : public CLobbyPackToPropagate
 	}
 };
 
+struct DLL_LINKAGE LobbyQuickLoadGame : public CLobbyPackToPropagate
+{
+	std::string saveFilePath;  //"Saves/Quicksave"
+
+	void visitTyped(ICPackVisitor & visitor) override;
+	
+	template <typename Handler> void serialize(Handler &h)
+	{
+			h & saveFilePath;
+	}
+};
+
 struct DLL_LINKAGE LobbyChangeHost : public CLobbyPackToPropagate
 {
 	GameConnectionID newHostConnectionId = GameConnectionID::INVALID;

+ 1 - 0
lib/serializer/RegisterTypes.h

@@ -297,6 +297,7 @@ void registerTypes(Serializer &s)
 	s.template registerType<BattleEnded>(255);
 	s.template registerType<RequestStatistic>(256);
 	s.template registerType<ResponseStatistic>(257);
+	s.template registerType<LobbyQuickLoadGame>(258);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 4 - 0
server/CVCMIServer.cpp

@@ -247,6 +247,10 @@ bool CVCMIServer::prepareToStartGame()
 			}
 			std::this_thread::sleep_for(std::chrono::milliseconds(50));
 		}
+		//send final progress
+		LobbyLoadProgress loadProgress;
+		loadProgress.progress = std::numeric_limits<Load::Type>::max();
+		announcePack(loadProgress);
 	});
 
 	auto newGH = std::make_shared<CGameHandler>(*this);

+ 3 - 0
server/LobbyNetPackVisitors.h

@@ -36,6 +36,7 @@ public:
 	}
 
 	void visitForLobby(CPackForLobby & pack) override;
+	void visitLobbyQuickLoadGame(LobbyQuickLoadGame & pack) override;
 	void visitLobbyClientConnected(LobbyClientConnected & pack) override;
 	void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override;
 	void visitLobbyRestartGame(LobbyRestartGame & pack) override;
@@ -62,6 +63,7 @@ public:
 	}
 
 	void visitForLobby(CPackForLobby & pack) override;
+	void visitLobbyQuickLoadGame(LobbyQuickLoadGame & pack) override;
 	void visitLobbyClientConnected(LobbyClientConnected & pack) override;
 	void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override;
 	void visitLobbyRestartGame(LobbyRestartGame & pack) override;
@@ -89,6 +91,7 @@ public:
 		return result;
 	}
 
+	void visitLobbyQuickLoadGame(LobbyQuickLoadGame & pack) override;
 	void visitLobbyClientConnected(LobbyClientConnected & pack) override;
 	void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override;
 	void visitLobbySetMap(LobbySetMap & pack) override;

+ 30 - 0
server/NetPacksLobbyServer.cpp

@@ -41,6 +41,12 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitForLobby(CPackForLobby & pac
 	}
 }
 
+void ClientPermissionsCheckerNetPackVisitor::visitLobbyQuickLoadGame(LobbyQuickLoadGame & pack)
+{
+	// only host can load quicksave
+	result = srv.isClientHost(connection->connectionID);
+}
+
 void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack)
 {
 	result = srv.getState() == EServerState::LOBBY;
@@ -110,6 +116,30 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyChatMessage(LobbyChatMess
 	result = true;
 }
 
+void ApplyOnServerNetPackVisitor::visitLobbyQuickLoadGame(LobbyQuickLoadGame & pack)
+{
+	// modify StartInfo to load the quicksave
+	srv.si->mode = EStartMode::LOAD_GAME;
+	srv.si->mapname = pack.saveFilePath;
+
+	// prepare game state (loads the save file)
+	if (!srv.prepareToStartGame()) {
+		//failure is destructive and an exception is the only way
+		throw std::runtime_error("Failed to prepare to start game during quick load.");
+	}
+
+	// create LobbyStartGame packet with loaded state and announce to all clients
+	LobbyStartGame startPack;
+	startPack.initializedStartInfo = std::make_shared<StartInfo>(*srv.gh->gameState().getInitialStartInfo());
+	startPack.initializedGameState = srv.gh->gs;
+	srv.announcePack(startPack);
+	result = true;
+}
+
+void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyQuickLoadGame(LobbyQuickLoadGame & pack)
+{
+}
+
 void ApplyOnServerNetPackVisitor::visitLobbySetMap(LobbySetMap & pack)
 {
 	if(srv.getState() != EServerState::LOBBY)