Răsfoiți Sursa

Merge pull request #2990 from Laserlicht/patch-4map_overview_rework

Map preview rework
Nordsoft91 2 ani în urmă
părinte
comite
d517ca9ba7

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

@@ -44,7 +44,7 @@
 	"vcmi.mainMenu.joinTCP" : "Připojit se do hry TCP/IP",
 	"vcmi.mainMenu.playerName" : "Hráč",
 	
-	"vcmi.lobby.filename" : "Název souboru",
+	"vcmi.lobby.filepath" : "Název souboru",
 	"vcmi.lobby.creationDate" : "Datum vytvoření",
 
 	"vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.",

+ 5 - 1
Mods/vcmi/config/vcmi/english.json

@@ -49,8 +49,12 @@
 	"vcmi.mainMenu.joinTCP" : "Join TCP/IP game",
 	"vcmi.mainMenu.playerName" : "Player",
 	
-	"vcmi.lobby.filename" : "Filename",
+	"vcmi.lobby.filepath" : "File path",
 	"vcmi.lobby.creationDate" : "Creation date",
+	"vcmi.lobby.scenarioName" : "Scenario name",
+	"vcmi.lobby.mapPreview" : "Map preview",
+	"vcmi.lobby.noPreview" : "no preview",
+	"vcmi.lobby.noUnderground" : "no underground",
 
 	"vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.",
 	"vcmi.server.errors.modsToEnable"    : "{Following mods are required}",

+ 5 - 1
Mods/vcmi/config/vcmi/german.json

@@ -48,8 +48,12 @@
 	"vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei",
 	"vcmi.mainMenu.playerName" : "Spieler",
 	
-	"vcmi.lobby.filename" : "Dateiname",
+	"vcmi.lobby.filepath" : "Dateipfad",
 	"vcmi.lobby.creationDate" : "Erstellungsdatum",
+	"vcmi.lobby.scenarioName" : "Szenario-Name",
+	"vcmi.lobby.mapPreview" : "Kartenvorschau",
+	"vcmi.lobby.noPreview" : "Keine Vorschau",
+	"vcmi.lobby.noUnderground" : "Kein Untergrund",
 
 	"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
 	"vcmi.server.errors.modsToEnable"    : "{Erforderliche Mods um das Spiel zu laden}",

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

@@ -43,7 +43,7 @@
 	"vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP",
 	"vcmi.mainMenu.playerName" : "Gracz",
 
-	"vcmi.lobby.filename" : "Nazwa pliku",
+	"vcmi.lobby.filepath" : "Nazwa pliku",
 	"vcmi.lobby.creationDate" : "Data utworzenia",
 
 	"vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",

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

@@ -44,7 +44,7 @@
 	"vcmi.mainMenu.joinTCP" : "Приєднатися до TCP/IP гри",
 	"vcmi.mainMenu.playerName" : "Гравець",
 	
-	"vcmi.lobby.filename" : "Назва файлу",
+	"vcmi.lobby.filepath" : "Назва файлу",
 	"vcmi.lobby.creationDate" : "Дата створення",
 
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",

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

@@ -50,7 +50,7 @@
   "vcmi.mainMenu.joinTCP": "Tham gia TCP/IP",
   "vcmi.mainMenu.playerName": "Người chơi",
 
-  "vcmi.lobby.filename": "Tên tập tin",
+  "vcmi.lobby.filepath": "Tên tập tin",
   "vcmi.lobby.creationDate": "Ngày tạo",
 
   "vcmi.server.errors.existingProcess": "1 chương trình VCMI khác đang chạy. Tắt nó trước khi mở cái mới",

+ 2 - 0
client/CMakeLists.txt

@@ -119,6 +119,7 @@ set(client_SRCS
 	windows/CHeroOverview.cpp
 	windows/CHeroWindow.cpp
 	windows/CKingdomInterface.cpp
+	windows/CMapOverview.cpp
 	windows/CMessage.cpp
 	windows/CPuzzleWindow.cpp
 	windows/CQuestLog.cpp
@@ -285,6 +286,7 @@ set(client_HEADERS
 	windows/CHeroWindow.h
 	windows/CKingdomInterface.h
 	windows/CMessage.h
+	windows/CMapOverview.h
 	windows/CPuzzleWindow.h
 	windows/CQuestLog.h
 	windows/CSpellWindow.h

+ 2 - 0
client/VCMI_client.cbp

@@ -229,6 +229,8 @@
 		<Unit filename="windows/CCreatureWindow.h" />
 		<Unit filename="windows/CHeroOverview.cpp" />
 		<Unit filename="windows/CHeroOverview.h" />
+		<Unit filename="windows/CMapOverview.cpp" />
+		<Unit filename="windows/CMapOverview.h" />
 		<Unit filename="windows/CHeroWindow.cpp" />
 		<Unit filename="windows/CHeroWindow.h" />
 		<Unit filename="windows/CKingdomInterface.cpp" />

+ 2 - 0
client/VCMI_client.vcxproj

@@ -238,6 +238,7 @@
     <ClCompile Include="windows\CCastleInterface.cpp" />
     <ClCompile Include="windows\CCreatureWindow.cpp" />
     <ClCompile Include="windows\CHeroOverview.cpp" />
+    <ClCompile Include="windows\CMapOverview.cpp" />
     <ClCompile Include="windows\CHeroWindow.cpp" />
     <ClCompile Include="windows\CKingdomInterface.cpp" />
     <ClCompile Include="windows\CQuestLog.cpp" />
@@ -305,6 +306,7 @@
     <ClInclude Include="windows\CCastleInterface.h" />
     <ClInclude Include="windows\CCreatureWindow.h" />
     <ClInclude Include="windows\CHeroOverview.h" />
+    <ClInclude Include="windows\CMapOverview.h" />
     <ClInclude Include="windows\CHeroWindow.h" />
     <ClInclude Include="windows\CKingdomInterface.h" />
     <ClInclude Include="windows\CQuestLog.h" />

+ 6 - 0
client/VCMI_client.vcxproj.filters

@@ -25,6 +25,9 @@
     <ClCompile Include="windows\CHeroOverview.cpp">
       <Filter>windows</Filter>
     </ClCompile>
+    <ClCompile Include="windows\CMapOverview.cpp">
+      <Filter>windows</Filter>
+    </ClCompile>
     <ClCompile Include="windows\CHeroWindow.cpp">
       <Filter>windows</Filter>
     </ClCompile>
@@ -179,6 +182,9 @@
     <ClInclude Include="windows\CHeroOverview.h">
       <Filter>windows</Filter>
     </ClInclude>
+    <ClInclude Include="windows\CMapOverview.h">
+      <Filter>windows</Filter>
+    </ClInclude>
     <ClInclude Include="windows\CHeroWindow.h">
       <Filter>windows</Filter>
     </ClInclude>

+ 29 - 0
client/gui/InterfaceObjectConfigurable.cpp

@@ -56,6 +56,8 @@ InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset)
 	REGISTER_BUILDER("layout", &InterfaceObjectConfigurable::buildLayout);
 	REGISTER_BUILDER("comboBox", &InterfaceObjectConfigurable::buildComboBox);
 	REGISTER_BUILDER("textInput", &InterfaceObjectConfigurable::buildTextInput);
+	REGISTER_BUILDER("transparentFilledRectangle", &InterfaceObjectConfigurable::buildTransparentFilledRectangle);
+	REGISTER_BUILDER("textBox", &InterfaceObjectConfigurable::buildTextBox);
 }
 
 void InterfaceObjectConfigurable::registerBuilder(const std::string & type, BuilderFunction f)
@@ -684,6 +686,33 @@ std::shared_ptr<CShowableAnim> InterfaceObjectConfigurable::buildAnimation(const
 	return anim;
 }
 
+std::shared_ptr<TransparentFilledRectangle> InterfaceObjectConfigurable::buildTransparentFilledRectangle(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget TransparentFilledRectangle");
+
+	auto rect = readRect(config["rect"]);
+	auto color = readColor(config["color"]);
+	if(!config["colorLine"].isNull())
+	{
+		auto colorLine = readColor(config["colorLine"]);
+		return std::make_shared<TransparentFilledRectangle>(rect, color, colorLine);
+	}
+	return std::make_shared<TransparentFilledRectangle>(rect, color);
+}
+
+std::shared_ptr<CTextBox> InterfaceObjectConfigurable::buildTextBox(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CTextBox");
+
+	auto rect = readRect(config["rect"]);
+	auto font = readFont(config["font"]);
+	auto alignment = readTextAlignment(config["alignment"]);
+	auto color = readColor(config["color"]);
+	auto text = readText(config["text"]);
+
+	return std::make_shared<CTextBox>(text, rect, 0, font, alignment, color);
+}
+
 std::shared_ptr<CIntObject> InterfaceObjectConfigurable::buildWidget(JsonNode config) const
 {
 	assert(!config.isNull());

+ 4 - 0
client/gui/InterfaceObjectConfigurable.h

@@ -29,6 +29,8 @@ class CShowableAnim;
 class CFilledTexture;
 class ComboBox;
 class CTextInput;
+class TransparentFilledRectangle;
+class CTextBox;
 
 #define REGISTER_BUILDER(type, method) registerBuilder(type, std::bind(method, this, std::placeholders::_1))
 
@@ -105,6 +107,8 @@ protected:
 	std::shared_ptr<CIntObject> buildLayout(const JsonNode &);
 	std::shared_ptr<ComboBox> buildComboBox(const JsonNode &);
 	std::shared_ptr<CTextInput> buildTextInput(const JsonNode &) const;
+	std::shared_ptr<TransparentFilledRectangle> buildTransparentFilledRectangle(const JsonNode & config) const;
+	std::shared_ptr<CTextBox> buildTextBox(const JsonNode & config) const;
 		
 	//composite widgets
 	std::shared_ptr<CIntObject> buildWidget(JsonNode config) const;

+ 2 - 126
client/lobby/SelectionTab.cpp

@@ -27,11 +27,10 @@
 #include "../widgets/TextControls.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
+#include "../windows/CMapOverview.h"
 #include "../render/CAnimation.h"
-#include "../render/Canvas.h"
 #include "../render/IImage.h"
 #include "../render/IRenderHandler.h"
-#include "../render/Graphics.h"
 
 #include "../../CCallback.h"
 
@@ -41,8 +40,6 @@
 #include "../../lib/GameSettings.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/campaign/CampaignState.h"
-#include "../../lib/mapping/CMap.h"
-#include "../../lib/mapping/CMapService.h"
 #include "../../lib/mapping/CMapInfo.h"
 #include "../../lib/mapping/CMapHeader.h"
 #include "../../lib/mapping/MapFormat.h"
@@ -366,13 +363,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition)
 		return;
 
 	if(!curItems[py]->isFolder)
-	{
-		std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getNameTranslated() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fullFileURI);
-		if(curItems[py]->date != "")
-			text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date);
-
-		GH.windows().createAndPushWindow<CMapInfoTooltipBox>(text, ResourcePath(curItems[py]->fileURI), tabType);
-	}
+		GH.windows().createAndPushWindow<CMapOverview>(curItems[py]->getNameTranslated(), curItems[py]->fullFileURI, curItems[py]->date, ResourcePath(curItems[py]->fileURI), tabType);
 	else
 		CRClickPopup::createAndPush(curItems[py]->folderName);
 }
@@ -822,121 +813,6 @@ std::unordered_set<ResourcePath> SelectionTab::getFiles(std::string dirURI, ERes
 	return ret;
 }
 
-SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourcePath resource, ESelectionScreen tabType)
-	: CWindowObject(BORDERED | RCLICK_POPUP)
-{
-	drawPlayerElements = tabType == ESelectionScreen::newGame;
-	renderImage = tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool();
-
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-
-	std::vector<std::shared_ptr<IImage>> mapLayerImages;
-	if(renderImage)
-		mapLayerImages = createMinimaps(ResourcePath(resource.getName(), EResType::MAP), IMAGE_SIZE);
-
-	if(mapLayerImages.size() == 0)
-		renderImage = false;
-
-	pos = Rect(0, 0, 3 * BORDER + 2 * IMAGE_SIZE, 2000);
-
-	auto drawLabel = [&]() {
-		label = std::make_shared<CTextBox>(text, Rect(BORDER, BORDER, BORDER + 2 * IMAGE_SIZE, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
-		if(!label->slider)
-			label->resize(Point(BORDER + 2 * IMAGE_SIZE, label->label->textSize.y));
-	};
-	drawLabel();
-
-	int textHeight = std::min(350, label->label->textSize.y);
-	pos.h = BORDER + textHeight + BORDER;
-	if(renderImage)
-		pos.h += IMAGE_SIZE + BORDER;
-	backgroundTexture = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), pos);
-	updateShadow();
-
-	drawLabel();
-
-	if(renderImage)
-	{
-		if(mapLayerImages.size() == 1)
-			image1 = std::make_shared<CPicture>(mapLayerImages[0], Point(BORDER + (BORDER + IMAGE_SIZE) / 2, textHeight + 2 * BORDER));
-		else
-		{
-			image1 = std::make_shared<CPicture>(mapLayerImages[0], Point(BORDER, textHeight + 2 * BORDER));
-			image2 = std::make_shared<CPicture>(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, textHeight + 2 * BORDER));
-		}
-	}
-
-	center(GH.getCursorPosition()); //center on mouse
-#ifdef VCMI_MOBILE
-	moveBy({0, -pos.h / 2});
-#endif
-	fitToScreen(10);
-}
-
-Canvas SelectionTab::CMapInfoTooltipBox::createMinimapForLayer(std::unique_ptr<CMap> & map, int layer)
-{
-	Canvas canvas = Canvas(Point(map->width, map->height));
-
-	for (int y = 0; y < map->height; ++y)
-		for (int x = 0; x < map->width; ++x)
-		{
-			TerrainTile & tile = map->getTile(int3(x, y, layer));
-
-			ColorRGBA color = tile.terType->minimapUnblocked;
-			if (tile.blocked && (!tile.visitable))
-				color = tile.terType->minimapBlocked;
-
-			if(drawPlayerElements)
-				// if object at tile is owned - it will be colored as its owner
-				for (const CGObjectInstance *obj : tile.blockingObjects)
-				{
-					PlayerColor player = obj->getOwner();
-					if(player == PlayerColor::NEUTRAL)
-					{
-						color = graphics->neutralColor;
-						break;
-					}
-					if (player.isValidPlayer())
-					{
-						color = graphics->playerColors[player.getNum()];
-						break;
-					}
-				}
-
-			canvas.drawPoint(Point(x, y), color);
-		}
-	
-	return canvas;
-}
-
-std::vector<std::shared_ptr<IImage>> SelectionTab::CMapInfoTooltipBox::createMinimaps(ResourcePath resource, int size)
-{
-	std::vector<std::shared_ptr<IImage>> ret = std::vector<std::shared_ptr<IImage>>();
-
-	CMapService mapService;
-	std::unique_ptr<CMap> map;
-	try
-	{
-		map = mapService.loadMap(resource);
-	}
-	catch (...)
-	{
-		return ret;
-	}
-
-	for(int i = 0; i < (map->twoLevel ? 2 : 1); i++)
-	{
-		Canvas canvas = createMinimapForLayer(map, i);
-		Canvas canvasScaled = Canvas(Point(size, size));
-		canvasScaled.drawScaled(canvas, Point(0, 0), Point(size, size));
-		std::shared_ptr<IImage> img = GH.renderHandler().createImage(canvasScaled.getInternalSurface());
-		
-		ret.push_back(img);
-	}
-
-	return ret;
-}
-
 SelectionTab::ListItem::ListItem(Point position, std::shared_ptr<CAnimation> iconsFormats, std::shared_ptr<CAnimation> iconsVictory, std::shared_ptr<CAnimation> iconsLoss)
 	: CIntObject(LCLICK, position)
 {

+ 0 - 19
client/lobby/SelectionTab.h

@@ -67,25 +67,6 @@ class SelectionTab : public CIntObject
 	// FIXME: CSelectionBase use them too!
 	std::shared_ptr<CAnimation> iconsVictoryCondition;
 	std::shared_ptr<CAnimation> iconsLossCondition;
-
-	class CMapInfoTooltipBox : public CWindowObject
-	{
-		const int IMAGE_SIZE = 169;
-		const int BORDER = 30;
-
-		bool drawPlayerElements;
-		bool renderImage;
-
-		std::shared_ptr<CFilledTexture> backgroundTexture;
-		std::shared_ptr<CTextBox> label;
-		std::shared_ptr<CPicture> image1;
-		std::shared_ptr<CPicture> image2;
-
-		Canvas createMinimapForLayer(std::unique_ptr<CMap> & map, int layer);
-		std::vector<std::shared_ptr<IImage>> createMinimaps(ResourcePath resource, int size);
-	public:
-		CMapInfoTooltipBox(std::string text, ResourcePath resource, ESelectionScreen tabType);
-	};
 public:
 	std::vector<std::shared_ptr<ElementInfo>> allItems;
 	std::vector<std::shared_ptr<ElementInfo>> curItems;

+ 206 - 0
client/windows/CMapOverview.cpp

@@ -0,0 +1,206 @@
+/*
+ * CMapOverview.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 "CMapOverview.h"
+
+#include "../lobby/SelectionTab.h"
+
+#include <vstd/DateUtils.h>
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../widgets/CComponent.h"
+#include "../widgets/MiscWidgets.h"
+#include "../widgets/TextControls.h"
+#include "../windows/GUIClasses.h"
+#include "../windows/InfoWindows.h"
+#include "../render/CAnimation.h"
+#include "../render/Canvas.h"
+#include "../render/IImage.h"
+#include "../render/IRenderHandler.h"
+#include "../render/Graphics.h"
+
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/campaign/CampaignState.h"
+#include "../../lib/mapping/CMap.h"
+#include "../../lib/mapping/CMapService.h"
+#include "../../lib/mapping/CMapInfo.h"
+#include "../../lib/mapping/CMapHeader.h"
+#include "../../lib/mapping/MapFormat.h"
+#include "../../lib/TerrainHandler.h"
+#include "../../lib/filesystem/Filesystem.h"
+
+#include "../../lib/serializer/BinaryDeserializer.h"
+#include "../../lib/StartInfo.h"
+#include "../../lib/rmg/CMapGenOptions.h"
+
+CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType)
+	: CWindowObject(BORDERED | RCLICK_POPUP), resource(resource), mapName(mapName), fileName(fileName), date(date), tabType(tabType)
+{
+
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	widget = std::make_shared<CMapOverviewWidget>(*this);
+
+	updateShadow();
+
+	center(GH.getCursorPosition()); //center on mouse
+#ifdef VCMI_MOBILE
+	moveBy({0, -pos.h / 2});
+#endif
+	fitToScreen(10);
+}
+
+Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr<CMap> & map, int layer) const
+{
+	Canvas canvas = Canvas(Point(map->width, map->height));
+
+	for (int y = 0; y < map->height; ++y)
+		for (int x = 0; x < map->width; ++x)
+		{
+			TerrainTile & tile = map->getTile(int3(x, y, layer));
+
+			ColorRGBA color = tile.terType->minimapUnblocked;
+			if (tile.blocked && (!tile.visitable))
+				color = tile.terType->minimapBlocked;
+
+			if(drawPlayerElements)
+				// if object at tile is owned - it will be colored as its owner
+				for (const CGObjectInstance *obj : tile.blockingObjects)
+				{
+					PlayerColor player = obj->getOwner();
+					if(player == PlayerColor::NEUTRAL)
+					{
+						color = graphics->neutralColor;
+						break;
+					}
+					if (player.isValidPlayer())
+					{
+						color = graphics->playerColors[player.getNum()];
+						break;
+					}
+				}
+
+			canvas.drawPoint(Point(x, y), color);
+		}
+	
+	return canvas;
+}
+
+std::vector<Canvas> CMapOverviewWidget::createMinimaps(ResourcePath resource) const
+{
+	std::vector<Canvas> ret = std::vector<Canvas>();
+
+	CMapService mapService;
+	std::unique_ptr<CMap> map;
+	try
+	{
+		map = mapService.loadMap(resource);
+	}
+	catch (const std::exception & e)
+	{
+		logGlobal->warn("Failed to generate map preview! %s", e.what());
+		return ret;
+	}
+
+	return createMinimaps(map);
+}
+
+std::vector<Canvas> CMapOverviewWidget::createMinimaps(std::unique_ptr<CMap> & map) const
+{
+	std::vector<Canvas> ret = std::vector<Canvas>();
+
+	for(int i = 0; i < (map->twoLevel ? 2 : 1); i++)
+		ret.push_back(createMinimapForLayer(map, i));
+
+	return ret;
+}
+
+std::shared_ptr<CPicture> CMapOverviewWidget::buildDrawMinimap(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget drawMinimap");
+
+	auto rect = readRect(config["rect"]);
+	auto id = config["id"].Integer();
+
+	if(id >= minimaps.size())
+		return nullptr;
+
+	Canvas canvasScaled = Canvas(Point(rect.w, rect.h));
+	canvasScaled.drawScaled(minimaps[id], Point(0, 0), Point(rect.w, rect.h));
+	std::shared_ptr<IImage> img = GH.renderHandler().createImage(canvasScaled.getInternalSurface());
+
+	return std::make_shared<CPicture>(img, Point(rect.x, rect.y));
+}
+
+CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent):
+	InterfaceObjectConfigurable(), p(parent)
+{
+	drawPlayerElements = p.tabType == ESelectionScreen::newGame;
+
+	const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json"));
+
+	if(settings["lobby"]["mapPreview"].Bool())
+	{
+		ResourcePath res = ResourcePath(p.resource.getName(), EResType::MAP);
+		std::unique_ptr<CMap> campaignMap = nullptr;
+		if(p.tabType != ESelectionScreen::newGame && config["variables"]["mapPreviewForSaves"].Bool())
+		{
+			CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION);
+			lf.checkMagicBytes(SAVEGAME_MAGIC);
+
+			std::unique_ptr<CMapHeader> mapHeader = std::make_unique<CMapHeader>();
+			StartInfo * startInfo;
+			lf >> *(mapHeader) >> startInfo;
+
+			if(startInfo->campState)
+				campaignMap = startInfo->campState->getMap(*startInfo->campState->currentScenario());
+			res = ResourcePath(startInfo->fileURI, EResType::MAP);
+		}
+		if(!campaignMap)
+			minimaps = createMinimaps(res);
+		else
+			minimaps = createMinimaps(campaignMap);
+	}
+
+	REGISTER_BUILDER("drawMinimap", &CMapOverviewWidget::buildDrawMinimap);
+
+	build(config);
+
+	if(auto w = widget<CFilledTexture>("background"))
+	{
+		p.pos = w->pos;
+	}
+	if(auto w = widget<CTextBox>("fileName"))
+	{
+		w->setText(p.fileName);
+	}
+	if(auto w = widget<CLabel>("mapName"))
+	{
+		w->setText(p.mapName);
+	}
+	if(auto w = widget<CLabel>("date"))
+	{
+		if(p.date.empty())
+		{
+			std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::MAP)));
+			w->setText(vstd::getFormattedDateTime(time));
+		}
+		else
+			w->setText(p.date);
+	}
+	if(auto w = widget<CLabel>("noUnderground"))
+	{
+		if(minimaps.size() == 0)
+			w->setText("");
+	}
+}

+ 59 - 0
client/windows/CMapOverview.h

@@ -0,0 +1,59 @@
+/*
+ * CMapOverview.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CMap;
+VCMI_LIB_NAMESPACE_END
+#include "CWindowObject.h"
+#include "../../lib/filesystem/ResourcePath.h"
+#include "../gui/InterfaceObjectConfigurable.h"
+
+class CSlider;
+class CLabel;
+class CPicture;
+class CFilledTexture;
+class CTextBox;
+class IImage;
+class Canvas;
+class TransparentFilledRectangle;
+enum ESelectionScreen : ui8;
+
+class CMapOverview;
+
+class CMapOverviewWidget : public InterfaceObjectConfigurable
+{
+	CMapOverview& p;
+
+	bool drawPlayerElements;
+	std::vector<Canvas> minimaps;
+
+	Canvas createMinimapForLayer(std::unique_ptr<CMap> & map, int layer) const;
+	std::vector<Canvas> createMinimaps(ResourcePath resource) const;
+	std::vector<Canvas> createMinimaps(std::unique_ptr<CMap> & map) const;
+
+	std::shared_ptr<CPicture> buildDrawMinimap(const JsonNode & config) const;
+public:
+	CMapOverviewWidget(CMapOverview& p);
+};
+
+class CMapOverview : public CWindowObject
+{
+	std::shared_ptr<CMapOverviewWidget> widget;
+
+public:
+	const ResourcePath resource;
+	const std::string mapName;
+	const std::string fileName;
+	const std::string date;
+	const ESelectionScreen tabType;
+
+	CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType);
+};

+ 156 - 0
config/widgets/mapOverview.json

@@ -0,0 +1,156 @@
+{
+	"items":
+	[		
+		{
+			"name": "background",
+			"type": "texture",
+			"image": "DIBOXBCK",
+			"rect": {"w": 428, "h": 379}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 5, "y": 5, "w": 418, "h": 20},
+			"color": [0, 0, 0, 75],
+			"colorLine": [128, 100, 75, 255]
+		},
+		{
+			"type": "label",
+			"font": "medium",
+			"alignment": "center",
+			"color": "yellow",
+			"text": "vcmi.lobby.scenarioName",
+			"position": {"x": 214, "y": 15}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 5, "y": 30, "w": 418, "h": 20},
+			"color": [0, 0, 0, 75],
+			"colorLine": [128, 100, 75, 255]
+		},
+		{
+			"type": "label",
+			"name": "mapName",
+			"font": "small",
+			"alignment": "center",
+			"color": "white",
+			"text": "",
+			"position": {"x": 214, "y": 40}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 5, "y": 55, "w": 418, "h": 20},
+			"color": [0, 0, 0, 75],
+			"colorLine": [128, 100, 75, 255]
+		},
+		{
+			"type": "label",
+			"font": "medium",
+			"alignment": "center",
+			"color": "yellow",
+			"text": "vcmi.lobby.mapPreview",
+			"position": {"x": 214, "y": 65}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 29, "y": 79, "w": 171, "h": 171},
+			"color": [0, 0, 0, 255],
+			"colorLine": [128, 100, 75, 255]
+		},
+		{
+			"type": "label",
+			"font": "small",
+			"alignment": "center",
+			"color": "white",
+			"text": "vcmi.lobby.noPreview",
+			"position": {"x": 114, "y": 164}
+		},
+		{
+			"type": "drawMinimap",
+			"id": 0,
+			"rect": {"x": 30, "y": 80, "w": 169, "h": 169}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 228, "y": 79, "w": 171, "h": 171},
+			"color": [0, 0, 0, 255],
+			"colorLine": [128, 100, 75, 255]
+		},
+		{
+			"type": "label",
+			"name": "noUnderground",
+			"font": "small",
+			"alignment": "center",
+			"color": "white",
+			"text": "vcmi.lobby.noUnderground",
+			"position": {"x": 313, "y": 164}
+		},
+		{
+			"type": "drawMinimap",
+			"id": 1,
+			"rect": {"x": 229, "y": 80, "w": 169, "h": 169}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 5, "y": 254, "w": 418, "h": 20},
+			"color": [0, 0, 0, 75],
+			"colorLine": [128, 100, 75, 255]
+		},
+		{
+			"type": "label",
+			"font": "medium",
+			"alignment": "center",
+			"color": "yellow",
+			"text": "vcmi.lobby.creationDate",
+			"position": {"x": 214, "y": 264}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 5, "y": 279, "w": 418, "h": 20},
+			"color": [0, 0, 0, 75],
+			"colorLine": [128, 100, 75, 255]
+		},
+		{
+			"type": "label",
+			"name": "date",
+			"font": "small",
+			"alignment": "center",
+			"color": "white",
+			"text": "",
+			"position": {"x": 214, "y": 289}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 5, "y": 304, "w": 418, "h": 20},
+			"color": [0, 0, 0, 75],
+			"colorLine": [128, 100, 75, 255]
+		},
+		{
+			"type": "label",
+			"font": "medium",
+			"alignment": "center",
+			"color": "yellow",
+			"text": "vcmi.lobby.filepath",
+			"position": {"x": 214, "y": 314}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 5, "y": 329, "w": 418, "h": 45},
+			"color": [0, 0, 0, 75],
+			"colorLine": [128, 100, 75, 255]
+		},
+		{
+			"type": "textBox",
+			"name": "fileName",
+			"font": "small",
+			"alignment": "center",
+			"color": "white",
+			"text": "",
+			"rect": {"x": 10, "y": 334, "w": 408, "h": 35}
+		}
+	],
+
+	"variables":
+	{
+		"mapPreviewForSaves": true
+	}
+}

+ 28 - 0
docs/modders/Configurable_Widgets.md

@@ -517,6 +517,22 @@ Configurable object has following structure:
 
 `"text"`: [text](#text),
 
+### TextBox
+
+`"type": "textBox"`
+
+`"name": "string"` optional, object name
+
+`"font"`: [font](#font)
+
+`"alignment"`: [alignment](#text-alignment),
+
+`"color"`: [color](#color),
+
+`"text"`: [text](#text),
+
+`"rect"`: [rect](#rect)
+
 ### Picture
 
 `"type": "picture"`
@@ -559,6 +575,18 @@ Filling area with texture
 
 `"rect"`: [rect](#rect)
 
+### TransparentFilledRectangle
+
+`"type": "transparentFilledRectangle"`
+
+`"name": "string"` optional, object name
+
+`"color"`: [color](#color) fill color of rectangle (supports transparency)
+
+`"colorLine"`: [color](#color) optional, 1px border color
+
+`"rect"`: [rect](#rect)
+
 ### Animation
 
 `"type": "animation"`

+ 3 - 1
lib/StartInfo.h

@@ -102,6 +102,7 @@ struct DLL_LINKAGE StartInfo
 	ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet
 	ui32 mapfileChecksum; //0 if not relevant
 	std::string startTimeIso8601;
+	std::string fileURI;
 	SimturnsInfo simturnsInfo;
 	TurnTimerInfo turnTimerInfo;
 	std::string mapname; // empty for random map, otherwise name of the map or savegame
@@ -127,6 +128,7 @@ struct DLL_LINKAGE StartInfo
 		h & seedPostInit;
 		h & mapfileChecksum;
 		h & startTimeIso8601;
+		h & fileURI;
 		h & simturnsInfo;
 		h & turnTimerInfo;
 		h & mapname;
@@ -135,7 +137,7 @@ struct DLL_LINKAGE StartInfo
 	}
 
 	StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0),
-		mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0)))
+		mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0))), fileURI("")
 	{
 
 	}

+ 2 - 0
server/CVCMIServer.cpp

@@ -318,6 +318,7 @@ bool CVCMIServer::prepareToStartGame()
 	case StartInfo::CAMPAIGN:
 		logNetwork->info("Preparing to start new campaign");
 		si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(0));
+		si->fileURI = mi->fileURI;
 		si->campState->setCurrentMap(campaignMap);
 		si->campState->setCurrentMapBonus(campaignBonus);
 		gh->init(si.get(), progressTracking);
@@ -326,6 +327,7 @@ bool CVCMIServer::prepareToStartGame()
 	case StartInfo::NEW_GAME:
 		logNetwork->info("Preparing to start new game");
 		si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(0));
+		si->fileURI = mi->fileURI;
 		gh->init(si.get(), progressTracking);
 		break;