Browse Source

Merge pull request #4543 from Laserlicht/chronicles_installer

Chronicles support
Ivan Savenko 1 year ago
parent
commit
c52533a961

+ 10 - 2
client/CServerHandler.cpp

@@ -532,7 +532,10 @@ void CServerHandler::sendGuiAction(ui8 action) const
 
 void CServerHandler::sendRestartGame() const
 {
-	GH.windows().createAndPushWindow<CLoadingScreen>();
+	if(si->campState && !si->campState->getLoadingBackground().empty())
+		GH.windows().createAndPushWindow<CLoadingScreen>(si->campState->getLoadingBackground());
+	else
+		GH.windows().createAndPushWindow<CLoadingScreen>();
 	
 	LobbyRestartGame endGame;
 	sendLobbyPack(endGame);
@@ -576,7 +579,12 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const
 	verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool());
 
 	if(!settings["session"]["headless"].Bool())
-		GH.windows().createAndPushWindow<CLoadingScreen>();
+	{
+		if(si->campState && !si->campState->getLoadingBackground().empty())
+			GH.windows().createAndPushWindow<CLoadingScreen>(si->campState->getLoadingBackground());
+		else
+			GH.windows().createAndPushWindow<CLoadingScreen>();
+	}
 	
 	LobbyPrepareStartGame lpsg;
 	sendLobbyPack(lpsg);

+ 5 - 1
client/NetPacksLobbyClient.cpp

@@ -35,6 +35,7 @@
 #include "../lib/CConfigHandler.h"
 #include "../lib/texts/CGeneralTextHandler.h"
 #include "../lib/serializer/Connection.h"
+#include "../lib/campaign/CampaignState.h"
 
 void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack)
 {
@@ -203,7 +204,10 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState &
 	if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN)
 	{
 		lobby->bonusSel = std::make_shared<CBonusSelection>();
-		GH.windows().pushWindow(lobby->bonusSel);
+		if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty())
+			GH.windows().createAndPushWindow<CampaignIntroVideo>(handler.si->campState->getIntroVideo(), handler.si->campState->getIntroVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getIntroVideoRim(), lobby->bonusSel);
+		else
+			GH.windows().pushWindow(lobby->bonusSel);
 	}
 
 	if(lobby->bonusSel)

+ 39 - 1
client/lobby/CBonusSelection.cpp

@@ -28,6 +28,7 @@
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/VideoWidget.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
 #include "../render/IImage.h"
@@ -58,6 +59,41 @@
 #include "../../lib/mapObjects/CGHeroInstance.h"
 
 
+CampaignIntroVideo::CampaignIntroVideo(VideoPath video, ImagePath rim, std::shared_ptr<CBonusSelection> bonusSel)
+	: CWindowObject(BORDERED), bonusSel(bonusSel)
+{
+	OBJECT_CONSTRUCTION;
+
+	addUsedEvents(LCLICK | KEYBOARD);
+
+	pos = center(Rect(0, 0, 800, 600));
+
+	videoPlayer = std::make_shared<VideoWidgetOnce>(Point(80, 186), video, true, [this](){ exit(); });
+	setBackground(rim);
+
+	CCS->musich->stopMusic();
+}
+
+void CampaignIntroVideo::exit()
+{
+	close();
+	
+	if (!CSH->si->campState->getMusic().empty())
+		CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false);
+
+	GH.windows().pushWindow(bonusSel);
+}
+
+void CampaignIntroVideo::clickPressed(const Point & cursorPosition)
+{
+	exit();
+}
+
+void CampaignIntroVideo::keyPressed(EShortcut key)
+{
+	exit();
+}
+
 std::shared_ptr<CampaignState> CBonusSelection::getCampaign()
 {
 	return CSH->si->campState;
@@ -93,7 +129,9 @@ CBonusSelection::CBonusSelection()
 	labelCampaignDescription = std::make_shared<CLabel>(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]);
 	campaignDescription = std::make_shared<CTextBox>(getCampaign()->getDescriptionTranslated(), Rect(480, 86, 286, 117), 1);
 
-	mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated(), 285);
+	bool videoButtonActive = CSH->getState() == EClientState::GAMEPLAY;
+	int availableSpace = videoButtonActive ? 225 : 285;
+	mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated(), availableSpace );
 	labelMapDescription = std::make_shared<CLabel>(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]);
 	mapDescription = std::make_shared<CTextBox>("", Rect(480, 278, 292, 108), 1);
 

+ 17 - 0
client/lobby/CBonusSelection.h

@@ -12,6 +12,7 @@
 #include "../windows/CWindowObject.h"
 
 #include "../lib/campaign/CampaignConstants.h"
+#include "../lib/filesystem/ResourcePath.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -28,6 +29,22 @@ class CLabel;
 class CFlagBox;
 class ISelectionScreenInfo;
 class ExtraOptionsTab;
+class VideoWidgetOnce;
+class CBonusSelection;
+
+
+class CampaignIntroVideo : public CWindowObject
+{
+	std::shared_ptr<VideoWidgetOnce> videoPlayer;
+	std::shared_ptr<CBonusSelection> bonusSel;
+
+	void exit();
+public:
+	CampaignIntroVideo(VideoPath video, ImagePath rim, std::shared_ptr<CBonusSelection> bonusSel);
+
+	void clickPressed(const Point & cursorPosition) override;
+	void keyPressed(EShortcut key) override;
+};
 
 /// Campaign screen where you can choose one out of three starting bonuses
 class CBonusSelection : public CWindowObject

+ 2 - 0
client/lobby/SelectionTab.cpp

@@ -787,6 +787,8 @@ bool SelectionTab::isMapSupported(const CMapInfo & info)
 			return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["supported"].Bool();
 		case EMapFormat::SOD:
 			return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["supported"].Bool();
+		case EMapFormat::CHR:
+			return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES)["supported"].Bool();
 		case EMapFormat::WOG:
 			return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["supported"].Bool();
 		case EMapFormat::HOTA:

+ 6 - 1
client/mainmenu/CMainMenu.cpp

@@ -629,7 +629,12 @@ void CSimpleJoinScreen::startConnection(const std::string & addr, ui16 port)
 }
 
 CLoadingScreen::CLoadingScreen()
-	: CWindowObject(BORDERED, getBackground())
+	: CLoadingScreen(getBackground())
+{
+}
+
+CLoadingScreen::CLoadingScreen(ImagePath background)
+	: CWindowObject(BORDERED, background)
 {
 	OBJECT_CONSTRUCTION;
 	

+ 1 - 0
client/mainmenu/CMainMenu.h

@@ -192,6 +192,7 @@ class CLoadingScreen : virtual public CWindowObject, virtual public Load::Progre
 
 public:	
 	CLoadingScreen();
+	CLoadingScreen(ImagePath background);
 	~CLoadingScreen();
 
 	void tick(uint32_t msPassed) override;

+ 254 - 0
config/campaignOverrides.json

@@ -0,0 +1,254 @@
+{
+	"MAPS/HC1_MAIN" : { // Heroes Chronicles 1
+		"regions":
+		{
+			"background": "chronicles_1/CamBkHc",
+			"prefix": "chronicles_1/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_1/ABVOFL4" },
+			{ "voiceProlog": "chronicles_1/H3X2UAE" },
+			{ "voiceProlog": "chronicles_1/H3X2BBA" },
+			{ "voiceProlog": "chronicles_1/H3X2RND" },
+			{ "voiceProlog": "chronicles_1/G1C" },
+			{ "voiceProlog": "chronicles_1/G2C" },
+			{ "voiceProlog": "chronicles_1/ABVOFL3" },
+			{ "voiceProlog": "chronicles_1/H3X2BBF", "voiceEpilog": "chronicles_1/N1C_D" }
+		],
+		"loadingBackground": "chronicles_1/LoadBar",
+		"introVideoRim": "chronicles_1/INTRORIM",
+		"introVideo": "chronicles_1/Intro"
+	},
+	"MAPS/HC2_MAIN" : { // Heroes Chronicles 2
+		"regions":
+		{
+			"background": "chronicles_2/CamBkHc",
+			"prefix": "chronicles_2/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_2/H3X2ELB" },
+			{ "voiceProlog": "chronicles_2/H3X2NBA" },
+			{ "voiceProlog": "chronicles_2/H3X2RNA" },
+			{ "voiceProlog": "chronicles_2/ABVOAB8" },
+			{ "voiceProlog": "chronicles_2/H3X2UAL" },
+			{ "voiceProlog": "chronicles_2/E1A" },
+			{ "voiceProlog": "chronicles_2/ABVOAB2" },
+			{ "voiceProlog": "chronicles_2/G1A", "voiceEpilog": "chronicles_2/S1C" }
+		],
+		"loadingBackground": "chronicles_2/LoadBar",
+		"introVideoRim": "chronicles_2/INTRORIM",
+		"introVideo": "chronicles_2/Intro"
+	},
+	"MAPS/HC3_MAIN" : { // Heroes Chronicles 3
+		"regions":
+		{
+			"background": "chronicles_3/CamBkHc",
+			"prefix": "chronicles_3/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_3/G2C" },
+			{ "voiceProlog": "chronicles_3/ABVOAB1" },
+			{ "voiceProlog": "chronicles_3/G2D" },
+			{ "voiceProlog": "chronicles_3/E1B" },
+			{ "voiceProlog": "chronicles_3/ABVOAB2" },
+			{ "voiceProlog": "chronicles_3/ABVOAB4" },
+			{ "voiceProlog": "chronicles_3/ABVOAB6" },
+			{ "voiceProlog": "chronicles_3/G3B", "voiceEpilog": "chronicles_3/ABVOFL2" }
+		],
+		"loadingBackground": "chronicles_3/LoadBar",
+		"introVideoRim": "chronicles_3/INTRORIM",
+		"introVideo": "chronicles_3/Intro"
+	},
+	"MAPS/HC4_MAIN" : { // Heroes Chronicles 4
+		"regions":
+		{
+			"background": "chronicles_4/CamBkHc",
+			"prefix": "chronicles_4/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_4/ABVOAB1" },
+			{ "voiceProlog": "chronicles_4/ABVODB4" },
+			{ "voiceProlog": "chronicles_4/H3X2ELC" },
+			{ "voiceProlog": "chronicles_4/ABVODS2" },
+			{ "voiceProlog": "chronicles_4/ABVODS1" },
+			{ "voiceProlog": "chronicles_4/ABVODS3" },
+			{ "voiceProlog": "chronicles_4/ABVODS4" },
+			{ "voiceProlog": "chronicles_4/H3X2NBD", "voiceEpilog": "chronicles_4/S1C" }
+		],
+		"loadingBackground": "chronicles_4/LoadBar",
+		"introVideoRim": "chronicles_4/INTRORIM",
+		"introVideo": "chronicles_4/Intro"
+	},
+	"MAPS/HC5_MAIN" : { // Heroes Chronicles 5
+		"regions":
+		{
+			"background": "chronicles_5/CamBkHc",
+			"prefix": "chronicles_5/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 34, "y": 184 },
+				{ "infix": "2", "x": 235, "y": 184 },
+				{ "infix": "3", "x": 34, "y": 320 },
+				{ "infix": "4", "x": 235, "y": 320 },
+				{ "infix": "5", "x": 129, "y": 459 }
+			]
+		},
+		"scenarioCount": 5,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_5/ABVOAB1" },
+			{ "voiceProlog": "chronicles_5/H3X2RNA" },
+			{ "voiceProlog": "chronicles_5/ABVOFL2" },
+			{ "voiceProlog": "chronicles_5/ABVOFL4" },
+			{ "voiceProlog": "chronicles_5/H3X2UAH", "voiceEpilog": "chronicles_5/N1C_D"  }
+		],
+		"loadingBackground": "chronicles_5/LoadBar",
+		"introVideoRim": "chronicles_5/INTRORIM",
+		"introVideo": "chronicles_5/Intro"
+	},
+	"MAPS/HC6_MAIN" : { // Heroes Chronicles 6
+		"regions":
+		{
+			"background": "chronicles_6/CamBkHc",
+			"prefix": "chronicles_6/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 34, "y": 184 },
+				{ "infix": "2", "x": 235, "y": 184 },
+				{ "infix": "3", "x": 34, "y": 320 },
+				{ "infix": "4", "x": 235, "y": 320 },
+				{ "infix": "5", "x": 129, "y": 459 }
+			]
+		},
+		"scenarioCount": 5,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_6/H3X2ELB" },
+			{ "voiceProlog": "chronicles_6/E1A" },
+			{ "voiceProlog": "chronicles_6/H3X2BBA" },
+			{ "voiceProlog": "chronicles_6/ABVOAB2" },
+			{ "voiceProlog": "chronicles_6/ABVOAB5", "voiceEpilog": "chronicles_6/ABVODB2"  }
+		],
+		"loadingBackground": "chronicles_6/LoadBar",
+		"introVideoRim": "chronicles_6/INTRORIM",
+		"introVideo": "chronicles_6/Intro"
+	},
+	"MAPS/HC7_MAIN" : { // Heroes Chronicles 7
+		"regions":
+		{
+			"background": "chronicles_7/CamBkHc",
+			"prefix": "chronicles_7/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_7/ABVOFL2" },
+			{ "voiceProlog": "chronicles_7/ABVOFL3" },
+			{ "voiceProlog": "chronicles_7/N1C_D" },
+			{ "voiceProlog": "chronicles_7/S1C" },
+			{ "voiceProlog": "chronicles_7/H3X2UAB" },
+			{ "voiceProlog": "chronicles_7/E2C" },
+			{ "voiceProlog": "chronicles_7/H3X2NBE" },
+			{ "voiceProlog": "chronicles_7/ABVOFW4", "voiceEpilog": "chronicles_7/ABVOAB1" }
+		],
+		"loadingBackground": "chronicles_7/LoadBar",
+		"introVideoRim": "chronicles_7/INTRORIM",
+		"introVideo": "chronicles_7/Intro5"
+	},
+	"MAPS/HC8_MAIN" : { // Heroes Chronicles 8
+		"regions":
+		{
+			"background": "chronicles_8/CamBkHc",
+			"prefix": "chronicles_8/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_8/H3X2RNB" },
+			{ "voiceProlog": "chronicles_8/ABVOAB9" },
+			{ "voiceProlog": "chronicles_8/H3X2BBB" },
+			{ "voiceProlog": "chronicles_8/ABVODS1" },
+			{ "voiceProlog": "chronicles_8/H3X2ELA" },
+			{ "voiceProlog": "chronicles_8/E1B" },
+			{ "voiceProlog": "chronicles_8/H3X2BBD" },
+			{ "voiceProlog": "chronicles_8/H3X2ELE", "voiceEpilog": "chronicles_8/ABVOAB7" }
+		],
+		"loadingBackground": "chronicles_8/LoadBar",
+		"introVideoRim": "chronicles_8/INTRORIM",
+		"introVideo": "chronicles_8/Intro6"
+	}
+}

+ 13 - 0
config/gameConfig.json

@@ -271,6 +271,19 @@
 					"portraitYoungYog" : 162
 				}
 			},
+			"chronicles" : { 
+				"supported" : true,
+				"iconIndex" : 2,
+				
+				"portraits" : {
+					"portraitTarnumBarbarian" : 163,
+					"portraitTarnumKnight" : 164,
+					"portraitTarnumWizard" : 165,
+					"portraitTarnumRanger" : 166,
+					"portraitTarnumOverlord" : 167,
+					"portraitTarnumBeastmaster" : 168
+				}
+			},
 			"jsonVCMI" : { 
 				"supported" : true,
 				"iconIndex" : 3

+ 174 - 0
config/heroes/portraits.json

@@ -202,5 +202,179 @@
 		],
 		"skills" : [],
 		"specialty" : {}
+	},
+	"portraitTarnumBarbarian" :
+	{
+		"class" : "barbarian",
+		"special" : true,
+		"images": {
+			"large" : "HPL137",
+			"small" : "HPS137",
+			"specialtySmall" : "default",
+			"specialtyLarge" : "default"
+		},
+		"texts" : {
+			"name" : "",
+			"biography" : "",
+			"specialty" : {
+				"description" : "",
+				"tooltip" : "",
+				"name" : ""
+			}
+		},
+		"army" : [
+			{
+				"creature" : "goblin",
+				"min" : 1,
+				"max" : 1
+			}
+		],
+		"skills" : [],
+		"specialty" : {}
+	},
+	"portraitTarnumKnight" :
+	{
+		"class" : "knight",
+		"special" : true,
+		"images": {
+			"large" : "HPL138",
+			"small" : "HPS138",
+			"specialtySmall" : "default",
+			"specialtyLarge" : "default"
+		},
+		"texts" : {
+			"name" : "",
+			"biography" : "",
+			"specialty" : {
+				"description" : "",
+				"tooltip" : "",
+				"name" : ""
+			}
+		},
+		"army" : [
+			{
+				"creature" : "pikeman",
+				"min" : 1,
+				"max" : 1
+			}
+		],
+		"skills" : [],
+		"specialty" : {}
+	},
+	"portraitTarnumWizard" :
+	{
+		"class" : "wizard",
+		"special" : true,
+		"images": {
+			"large" : "HPL139",
+			"small" : "HPS139",
+			"specialtySmall" : "default",
+			"specialtyLarge" : "default"
+		},
+		"texts" : {
+			"name" : "",
+			"biography" : "",
+			"specialty" : {
+				"description" : "",
+				"tooltip" : "",
+				"name" : ""
+			}
+		},
+		"army" : [
+			{
+				"creature" : "enchanter",
+				"min" : 1,
+				"max" : 1
+			}
+		],
+		"skills" : [],
+		"specialty" : {}
+	},
+	"portraitTarnumRanger" :
+	{
+		"class" : "ranger",
+		"special" : true,
+		"images": {
+			"large" : "HPL140",
+			"small" : "HPS140",
+			"specialtySmall" : "default",
+			"specialtyLarge" : "default"
+		},
+		"texts" : {
+			"name" : "",
+			"biography" : "",
+			"specialty" : {
+				"description" : "",
+				"tooltip" : "",
+				"name" : ""
+			}
+		},
+		"army" : [
+			{
+				"creature" : "sharpshooter",
+				"min" : 1,
+				"max" : 1
+			}
+		],
+		"skills" : [],
+		"specialty" : {}
+	},
+	"portraitTarnumOverlord" :
+	{
+		"class" : "overlord",
+		"special" : true,
+		"images": {
+			"large" : "HPL141",
+			"small" : "HPS141",
+			"specialtySmall" : "default",
+			"specialtyLarge" : "default"
+		},
+		"texts" : {
+			"name" : "",
+			"biography" : "",
+			"specialty" : {
+				"description" : "",
+				"tooltip" : "",
+				"name" : ""
+			}
+		},
+		"army" : [
+			{
+				"creature" : "troglodyte",
+				"min" : 1,
+				"max" : 1
+			}
+		],
+		"skills" : [],
+		"specialty" : {}
+	},
+	"portraitTarnumBeastmaster" :
+	{
+		"class" : "beastmaster",
+		"special" : true,
+		"images": {
+			"large" : "HPL142",
+			"small" : "HPS142",
+			"specialtySmall" : "default",
+			"specialtyLarge" : "default"
+		},
+		"texts" : {
+			"name" : "",
+			"biography" : "",
+			"specialty" : {
+				"description" : "",
+				"tooltip" : "",
+				"name" : ""
+			}
+		},
+		"army" : [
+			{
+				"creature" : "gnoll",
+				"min" : 1,
+				"max" : 1
+			}
+		],
+		"skills" : [],
+		"specialty" : {}
 	}
 }

+ 3 - 0
docs/modders/Campaign_Format.md

@@ -52,6 +52,9 @@ In header are parameters describing campaign properties
 - `"campaignVersion"` is creator defined version
 - `"creationDateTime"` unix time of campaign creation
 - `"allowDifficultySelection"` is a boolean field (`true`/`false`) which allows or disallows to choose difficulty before scenario start
+- `"loadingBackground"` is for setting a different loading screen background
+- `"introVideo"` is for defining an optional intro video
+- `"introVideoRim"` is for the Rim around the optional video (default is INTRORIM)
 
 ## Scenario description
 

+ 4 - 0
launcher/CMakeLists.txt

@@ -12,10 +12,12 @@ set(launcher_SRCS
 		modManager/cmodlistview_moc.cpp
 		modManager/cmodmanager.cpp
 		modManager/imageviewer_moc.cpp
+		modManager/chroniclesextractor.cpp
 		settingsView/csettingsview_moc.cpp
 		firstLaunch/firstlaunch_moc.cpp
 		main.cpp
 		helper.cpp
+		innoextract.cpp
 		mainwindow_moc.cpp
 		languages.cpp
 		launcherdirs.cpp
@@ -42,6 +44,7 @@ set(launcher_HEADERS
 		modManager/cmodlistview_moc.h
 		modManager/cmodmanager.h
 		modManager/imageviewer_moc.h
+		modManager/chroniclesextractor.h
 		settingsView/csettingsview_moc.h
 		firstLaunch/firstlaunch_moc.h
 		mainwindow_moc.h
@@ -51,6 +54,7 @@ set(launcher_HEADERS
 		updatedialog_moc.h
 		main.h
 		helper.h
+		innoextract.h
 		prepare.h
 )
 

+ 6 - 43
launcher/firstLaunch/firstlaunch_moc.cpp

@@ -21,11 +21,7 @@
 #include "../../lib/filesystem/Filesystem.h"
 #include "../helper.h"
 #include "../languages.h"
-
-#ifdef ENABLE_INNOEXTRACT
-#include "cli/extract.hpp"
-#include "setup/version.hpp"
-#endif
+#include "../innoextract.h"
 
 #ifdef VCMI_IOS
 #include "ios/selectdirectory.h"
@@ -386,44 +382,11 @@ void FirstLaunchView::extractGogData()
 		if(isGogGalaxyExe(tmpFileExe))
 			errorText = tr("You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer!");
 
-		::extract_options o;
-		o.extract = true;
-
-		// standard settings
-		o.gog_galaxy = true;
-		o.codepage = 0U;
-		o.output_dir = tempDir.path().toStdString();
-		o.extract_temp = true;
-		o.extract_unknown = true;
-		o.filenames.set_expand(true);
-
-		o.preserve_file_times = true; // also correctly closes file -> without it: on Windows the files are not written completely
-
-		try
-		{
-			if(errorText.isEmpty())
-				process_file(tmpFileExe.toStdString(), o, [this](float progress) {
-					ui->progressBarGog->setValue(progress * 100);
-					qApp->processEvents();
-				});
-		}
-		catch(const std::ios_base::failure & e)
-		{
-			errorText = tr("Stream error while extracting files!\nerror reason: ");
-			errorText += e.what();
-		}
-		catch(const format_error & e)
-		{
-			errorText = e.what();
-		}
-		catch(const std::runtime_error & e)
-		{
-			errorText = e.what();
-		}
-		catch(const setup::version_error &)
-		{
-			errorText = tr("Not a supported Inno Setup installer!");
-		}
+		if(errorText.isEmpty())
+			errorText = Innoextract::extract(tmpFileExe, tempDir.path(), [this](float progress) {
+				ui->progressBarGog->setValue(progress * 100);
+				qApp->processEvents();
+			});
 
 		ui->progressBarGog->setVisible(false);
 		ui->pushButtonGogInstall->setVisible(true);

+ 62 - 0
launcher/innoextract.cpp

@@ -0,0 +1,62 @@
+/*
+ * innoextract.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 "innoextract.h"
+
+#ifdef ENABLE_INNOEXTRACT
+#include "cli/extract.hpp"
+#include "setup/version.hpp"
+#endif
+
+QString Innoextract::extract(QString installer, QString outDir, std::function<void (float percent)> cb)
+{
+	QString errorText{};
+
+#ifdef ENABLE_INNOEXTRACT
+	::extract_options o;
+	o.extract = true;
+
+	// standard settings
+	o.gog_galaxy = true;
+	o.codepage = 0U;
+	o.output_dir = outDir.toStdString();
+	o.extract_temp = true;
+	o.extract_unknown = true;
+	o.filenames.set_expand(true);
+
+	o.preserve_file_times = true; // also correctly closes file -> without it: on Windows the files are not written completely
+
+	try
+	{
+		process_file(installer.toStdString(), o, cb);
+	}
+	catch(const std::ios_base::failure & e)
+	{
+		errorText = tr("Stream error while extracting files!\nerror reason: ");
+		errorText += e.what();
+	}
+	catch(const format_error & e)
+	{
+		errorText = e.what();
+	}
+	catch(const std::runtime_error & e)
+	{
+		errorText = e.what();
+	}
+	catch(const setup::version_error &)
+	{
+		errorText = tr("Not a supported Inno Setup installer!");
+	}
+#else
+	errorText = tr("VCMI was compiled without innoextract support, which is needed to extract exe files!");
+#endif
+
+	return errorText;
+}

+ 16 - 0
launcher/innoextract.h

@@ -0,0 +1,16 @@
+/*
+ * innoextract.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
+
+class Innoextract : public QObject
+{
+public:
+	static QString extract(QString installer, QString outDir, std::function<void (float percent)> cb = nullptr);
+};

+ 225 - 0
launcher/modManager/chroniclesextractor.cpp

@@ -0,0 +1,225 @@
+/*
+ * chroniclesextractor.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 "chroniclesextractor.h"
+
+#include "../../lib/VCMIDirs.h"
+#include "../../lib/filesystem/CArchiveLoader.h"
+
+#include "../innoextract.h"
+
+ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb) :
+	parent(p), cb(cb)
+{
+}
+
+bool ChroniclesExtractor::createTempDir()
+{
+	tempDir = QDir(pathToQString(VCMIDirs::get().userDataPath()));
+	if(tempDir.cd("tmp"))
+	{
+		tempDir.removeRecursively(); // remove if already exists (e.g. previous run)
+		tempDir.cdUp();
+	}
+	tempDir.mkdir("tmp");
+	if(!tempDir.cd("tmp"))
+		return false; // should not happen - but avoid deleting wrong folder in any case
+
+	return true;
+}
+
+void ChroniclesExtractor::removeTempDir()
+{
+	tempDir.removeRecursively();
+}
+
+int ChroniclesExtractor::getChronicleNo(QFile & file)
+{
+	if(!file.open(QIODevice::ReadOnly))
+	{
+		QMessageBox::critical(parent, tr("File cannot opened"), file.errorString());
+		return 0;
+	}
+
+	QByteArray magic{"MZ"};
+	QByteArray magicFile = file.read(magic.length());
+	if(!magicFile.startsWith(magic))
+	{
+		QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select an gog installer file!"));
+		return 0;
+	}
+
+	QByteArray dataBegin = file.read(1'000'000);
+	int chronicle = 0;
+	for (const auto& kv : chronicles) {
+		if(dataBegin.contains(kv.second))
+		{
+			chronicle = kv.first;
+			break;
+		}
+	}
+	if(!chronicle)
+	{
+		QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select an chronicle installer file!"));
+		return 0;
+	}
+	return chronicle;
+}
+
+bool ChroniclesExtractor::extractGogInstaller(QString file)
+{
+	QString errorText = Innoextract::extract(file, tempDir.path(), [this](float progress) {
+		float overallProgress = ((1.0 / static_cast<float>(fileCount)) * static_cast<float>(extractionFile)) + (progress / static_cast<float>(fileCount));
+		if(cb)
+			cb(overallProgress);
+	});
+
+	if(!errorText.isEmpty())
+	{
+		QMessageBox::critical(parent, tr("Extracting error!"), errorText);
+		return false;
+	}
+
+	return true;
+}
+
+void ChroniclesExtractor::createBaseMod() const
+{
+	QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods"));
+	dir.mkdir("chronicles");
+	dir.cd("chronicles");
+	dir.mkdir("Mods");
+
+	QJsonObject mod
+	{
+		{ "modType", "Expansion" },
+		{ "name", tr("Heroes Chronicles") },
+		{ "description", tr("Heroes Chronicles") },
+		{ "author", "3DO" },
+		{ "version", "1.0" },
+		{ "contact", "vcmi.eu" },
+	};
+
+	QFile jsonFile(dir.filePath("mod.json"));
+    jsonFile.open(QFile::WriteOnly);
+    jsonFile.write(QJsonDocument(mod).toJson());
+}
+
+void ChroniclesExtractor::createChronicleMod(int no)
+{
+	QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / ("chronicles_" + std::to_string(no))));
+	dir.removeRecursively();
+	dir.mkpath(".");
+
+	QByteArray tmpChronicles = chronicles.at(no);
+	tmpChronicles.replace('\0', "");
+
+	QJsonObject mod
+	{
+		{ "modType", "Expansion" },
+		{ "name", QString::number(no) + " - " + QString(tmpChronicles) },
+		{ "description", tr("Heroes Chronicles") + " - " + QString::number(no) + " - " + QString(tmpChronicles) },
+		{ "author", "3DO" },
+		{ "version", "1.0" },
+		{ "contact", "vcmi.eu" },
+	};
+	
+	QFile jsonFile(dir.filePath("mod.json"));
+    jsonFile.open(QFile::WriteOnly);
+    jsonFile.write(QJsonDocument(mod).toJson());
+
+	dir.cd("content");
+	
+	extractFiles(no);
+}
+
+void ChroniclesExtractor::extractFiles(int no) const
+{
+	QByteArray tmpChronicles = chronicles.at(no);
+	tmpChronicles.replace('\0', "");
+
+	std::string chroniclesDir = "chronicles_" + std::to_string(no);
+	QDir tmpDir = tempDir.filePath(tempDir.entryList({"app"}, QDir::Filter::Dirs).front());
+	tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({QString(tmpChronicles)}, QDir::Filter::Dirs).front()));
+	tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front()));
+	auto basePath = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / chroniclesDir / "content";
+	QDir outDirDataPortraits(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "Data"));
+	QDir outDirData(pathToQString(basePath / "Data" / chroniclesDir));
+	QDir outDirSprites(pathToQString(basePath / "Sprites" / chroniclesDir));
+	QDir outDirVideo(pathToQString(basePath / "Video" / chroniclesDir));
+	QDir outDirSounds(pathToQString(basePath / "Sounds" / chroniclesDir));
+	QDir outDirMaps(pathToQString(basePath / "Maps"));
+
+	auto extract = [](QDir scrDir, QDir dest, QString file, std::vector<std::string> files = {}){
+		CArchiveLoader archive("", scrDir.filePath(scrDir.entryList({file}).front()).toStdString(), false);
+		for(auto & entry : archive.getEntries())
+			if(files.empty())
+				archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
+			else
+			{
+				for(const auto & item : files)
+					if(boost::algorithm::to_lower_copy(entry.second.name).find(boost::algorithm::to_lower_copy(item)) != std::string::npos)
+						archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
+			}
+	};
+
+	extract(tmpDir, outDirData, "xBitmap.lod");
+	extract(tmpDir, outDirData, "xlBitmap.lod");
+	extract(tmpDir, outDirSprites, "xSprite.lod");
+	extract(tmpDir, outDirSprites, "xlSprite.lod");
+	extract(tmpDir, outDirVideo, "xVideo.vid");
+	extract(tmpDir, outDirSounds, "xSound.snd");
+
+	tmpDir.cdUp();
+	if(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).size()) // special case for "The World Tree": the map is in the "Maps" folder instead of inside the lod
+	{
+		QDir tmpDirMaps = tmpDir.filePath(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).front());
+		for(const auto & entry : tmpDirMaps.entryList())
+			QFile(tmpDirMaps.filePath(entry)).copy(outDirData.filePath(entry));
+	}
+
+	tmpDir.cdUp();
+	QDir tmpDirData = tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front());
+	auto tarnumPortraits = std::vector<std::string>{"HPS137", "HPS138", "HPS139", "HPS140", "HPS141", "HPS142", "HPL137", "HPL138", "HPL139", "HPL140", "HPL141", "HPL142"};
+	extract(tmpDirData, outDirDataPortraits, "bitmap.lod", tarnumPortraits);
+	extract(tmpDirData, outDirData, "lbitmap.lod", std::vector<std::string>{"INTRORIM"});
+
+	if(!outDirMaps.exists())
+		outDirMaps.mkpath(".");
+	QString campaignFileName = "Hc" + QString::number(no) + "_Main.h3c";
+	QFile(outDirData.filePath(outDirData.entryList({"Main.h3c"}).front())).copy(outDirMaps.filePath(campaignFileName));
+}
+
+void ChroniclesExtractor::installChronicles(QStringList exe)
+{
+	extractionFile = -1;
+	fileCount = exe.size();
+	for(QString f : exe)
+	{
+		extractionFile++;
+		QFile file(f);
+
+		int chronicleNo = getChronicleNo(file);
+		if(!chronicleNo)
+			continue;
+
+		if(!createTempDir())
+			continue;
+
+		if(!extractGogInstaller(f))
+			continue;
+		
+		createBaseMod();
+		createChronicleMod(chronicleNo);
+
+		removeTempDir();
+	}
+}

+ 47 - 0
launcher/modManager/chroniclesextractor.h

@@ -0,0 +1,47 @@
+/*
+ * chroniclesextractor.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 "../StdInc.h"
+
+class ChroniclesExtractor : public QObject
+{
+	Q_OBJECT
+
+	QWidget *parent;
+	std::function<void(float percent)> cb;
+
+	QDir tempDir;
+	int extractionFile;
+	int fileCount;
+
+	bool createTempDir();
+	void removeTempDir();
+	int getChronicleNo(QFile & file);
+	bool extractGogInstaller(QString filePath);
+	void createBaseMod() const;
+	void createChronicleMod(int no);
+	void extractFiles(int no) const;
+
+	const std::map<int, QByteArray> chronicles = {
+		{1, QByteArray{reinterpret_cast<const char*>(u"Warlords of the Wasteland"), 50}},
+		{2, QByteArray{reinterpret_cast<const char*>(u"Conquest of the Underworld"), 52}},
+		{3, QByteArray{reinterpret_cast<const char*>(u"Masters of the Elements"), 46}},
+		{4, QByteArray{reinterpret_cast<const char*>(u"Clash of the Dragons"), 40}},
+		{5, QByteArray{reinterpret_cast<const char*>(u"The World Tree"), 28}},
+		{6, QByteArray{reinterpret_cast<const char*>(u"The Fiery Moon"), 28}},
+		{7, QByteArray{reinterpret_cast<const char*>(u"Revolt of the Beastmasters"), 52}},
+		{8, QByteArray{reinterpret_cast<const char*>(u"The Sword of Frost"), 36}}
+	};
+public:
+	void installChronicles(QStringList exe);
+
+	ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb = nullptr);
+};

+ 47 - 3
launcher/modManager/cmodlistview_moc.cpp

@@ -20,6 +20,7 @@
 #include "cmodlistmodel_moc.h"
 #include "cmodmanager.h"
 #include "cdownloadmanager_moc.h"
+#include "chroniclesextractor.h"
 #include "../settingsView/csettingsview_moc.h"
 #include "../launcherdirs.h"
 #include "../jsonutils.h"
@@ -29,6 +30,9 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/texts/Languages.h"
 #include "../../lib/modding/CModVersion.h"
+#include "../../lib/filesystem/Filesystem.h"
+
+#include <future>
 
 static double mbToBytes(double mb)
 {
@@ -55,7 +59,7 @@ void CModListView::dragEnterEvent(QDragEnterEvent* event)
 {
 	if(event->mimeData()->hasUrls())
 		for(const auto & url : event->mimeData()->urls())
-			for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json"}))
+			for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json", ".exe"}))
 				if(url.fileName().endsWith(ending, Qt::CaseInsensitive))
 				{
 					event->acceptProposedAction();
@@ -636,8 +640,16 @@ void CModListView::on_installFromFileButton_clicked()
 	// https://bugreports.qt.io/browse/QTBUG-98651
 	QTimer::singleShot(0, this, [this]
 	{
-		QString filter = tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json);;" + tr("Maps") + " (*.h3m *.vmap);;" + tr("Campaigns") + " (*.h3c *.vcmp);;" + tr("Configs") + " (*.json);;" + tr("Mods") + " (*.zip)";
-		QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns) to install..."), QDir::homePath(), filter);
+		QString filter = tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json *.exe);;" + 
+			tr("Maps") + " (*.h3m *.vmap);;" + 
+			tr("Campaigns") + " (*.h3c *.vcmp);;" + 
+			tr("Configs") + " (*.json);;" + 
+			tr("Mods") + " (*.zip);;" + 
+			tr("Gog files") + " (*.exe)";
+#if defined(VCMI_MOBILE)
+		filter = tr("All files (*.*)"); //Workaround for sometimes incorrect mime for some extensions (e.g. for exe)
+#endif
+		QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns, gog files) to install..."), QDir::homePath(), filter);
 
 		for(const auto & file : files)
 		{
@@ -786,6 +798,7 @@ void CModListView::installFiles(QStringList files)
 	QStringList mods;
 	QStringList maps;
 	QStringList images;
+	QStringList exe;
 	QVector<QVariantMap> repositories;
 
 	// TODO: some better way to separate zip's with mods and downloaded repository files
@@ -795,6 +808,8 @@ void CModListView::installFiles(QStringList files)
 			mods.push_back(filename);
 		else if(filename.endsWith(".h3m", Qt::CaseInsensitive) || filename.endsWith(".h3c", Qt::CaseInsensitive) || filename.endsWith(".vmap", Qt::CaseInsensitive) || filename.endsWith(".vcmp", Qt::CaseInsensitive))
 			maps.push_back(filename);
+		if(filename.endsWith(".exe", Qt::CaseInsensitive))
+			exe.push_back(filename);
 		else if(filename.endsWith(".json", Qt::CaseInsensitive))
 		{
 			//download and merge additional files
@@ -832,6 +847,35 @@ void CModListView::installFiles(QStringList files)
 	if(!maps.empty())
 		installMaps(maps);
 
+	if(!exe.empty())
+	{
+		ui->progressBar->setFormat(tr("Installing chronicles"));
+
+		float prog = 0.0;
+
+		auto futureExtract = std::async(std::launch::async, [this, exe, &prog]()
+		{
+			ChroniclesExtractor ce(this, [&prog](float progress) { prog = progress; });
+			ce.installChronicles(exe);
+			return true;
+		});
+		
+		while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready)
+		{
+			emit extractionProgress(static_cast<int>(prog * 1000.f), 1000);
+			qApp->processEvents();
+		}
+		
+		if(futureExtract.get())
+		{
+			//update
+			CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &){ return true; });
+			manager->loadMods();
+			modModel->reloadRepositories();
+			emit modsChanged();
+		}
+	}
+
 	if(!images.empty())
 		loadScreenshots();
 }

+ 1 - 0
lib/GameSettings.cpp

@@ -79,6 +79,7 @@ void GameSettings::load(const JsonNode & input)
 		{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA,      "mapFormat", "restorationOfErathia"             },
 		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,           "mapFormat", "armageddonsBlade"                 },
 		{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH,             "mapFormat", "shadowOfDeath"                    },
+		{EGameSettings::MAP_FORMAT_CHRONICLES,                  "mapFormat", "chronicles"                       },
 		{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS,           "mapFormat", "hornOfTheAbyss"                   },
 		{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS,         "mapFormat", "inTheWakeOfGods"                  },
 		{EGameSettings::MAP_FORMAT_JSON_VCMI,                   "mapFormat", "jsonVCMI"                         },

+ 1 - 0
lib/GameSettings.h

@@ -58,6 +58,7 @@ enum class EGameSettings
 	MAP_FORMAT_RESTORATION_OF_ERATHIA,
 	MAP_FORMAT_ARMAGEDDONS_BLADE,
 	MAP_FORMAT_SHADOW_OF_DEATH,
+	MAP_FORMAT_CHRONICLES,
 	MAP_FORMAT_HORN_OF_THE_ABYSS,
 	MAP_FORMAT_JSON_VCMI,
 	MAP_FORMAT_IN_THE_WAKE_OF_GODS,

+ 1 - 1
lib/campaign/CampaignConstants.h

@@ -18,7 +18,7 @@ enum class CampaignVersion : uint8_t
 	AB = 5,
 	SoD = 6,
 	WoG = 6,
-	//		Chr = 7, // Heroes Chronicles, likely identical to SoD, untested
+	Chr = 7,
 
 	VCMI = 1,
 	VCMI_MIN = 1,

+ 7 - 1
lib/campaign/CampaignHandler.cpp

@@ -38,12 +38,14 @@ void CampaignHandler::readCampaign(Campaign * ret, const std::vector<ui8> & inpu
 		CBinaryReader reader(&stream);
 
 		readHeaderFromMemory(*ret, reader, filename, modName, encoding);
+		ret->overrideCampaign();
 
 		for(int g = 0; g < ret->numberOfScenarios; ++g)
 		{
 			auto scenarioID = static_cast<CampaignScenarioID>(ret->scenarios.size());
 			ret->scenarios[scenarioID] = readScenarioFromMemory(reader, *ret);
 		}
+		ret->overrideCampaignScenarios();
 	}
 	else // text format (json)
 	{
@@ -166,6 +168,9 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader
 	ret.filename = filename;
 	ret.modName = modName;
 	ret.encoding = encoding;
+	ret.loadingBackground = ImagePath::fromJson(reader["loadingBackground"]);
+	ret.introVideoRim = ImagePath::fromJson(reader["introVideoRim"]);
+	ret.introVideo = VideoPath::fromJson(reader["introVideo"]);
 }
 
 CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader)
@@ -392,7 +397,8 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader
 {
 	ret.version = static_cast<CampaignVersion>(reader.readUInt32());
 	ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19]
-	ret.loadLegacyData(campId);
+	if(ret.version != CampaignVersion::Chr) // For chronicles: Will be overridden later; Chronicles uses own logic (reusing OH3 ID's)
+		ret.loadLegacyData(campId);
 	ret.name.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "name"));
 	ret.description.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "description"));
 	ret.author.appendRawString("");

+ 61 - 0
lib/campaign/CampaignState.cpp

@@ -20,6 +20,7 @@
 #include "../mapObjects/CGHeroInstance.h"
 #include "../serializer/JsonDeserializer.h"
 #include "../serializer/JsonSerializer.h"
+#include "../json/JsonUtils.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -138,6 +139,12 @@ void CampaignHeader::loadLegacyData(ui8 campId)
 	numberOfScenarios = VLC->generaltexth->getCampaignLength(campId);
 }
 
+void CampaignHeader::loadLegacyData(CampaignRegions regions, int numOfScenario)
+{
+	campaignRegions = regions;
+	numberOfScenarios = numOfScenario;
+}
+
 bool CampaignHeader::playerSelectedDifficulty() const
 {
 	return difficultyChosenByPlayer;
@@ -198,6 +205,21 @@ AudioPath CampaignHeader::getMusic() const
 	return music;
 }
 
+ImagePath CampaignHeader::getLoadingBackground() const
+{
+	return loadingBackground;
+}
+
+ImagePath CampaignHeader::getIntroVideoRim() const
+{
+	return introVideoRim;
+}
+
+VideoPath CampaignHeader::getIntroVideo() const
+{
+	return introVideo;
+}
+
 const CampaignRegions & CampaignHeader::getRegions() const
 {
 	return campaignRegions;
@@ -455,6 +477,45 @@ std::set<CampaignScenarioID> Campaign::allScenarios() const
 	return result;
 }
 
+void Campaign::overrideCampaign()
+{
+	const JsonNode node = JsonUtils::assembleFromFiles("config/campaignOverrides.json");
+	for (auto & entry : node.Struct())
+		if(filename == entry.first)
+		{
+			if(!entry.second["regions"].isNull() && !entry.second["scenarioCount"].isNull())
+				loadLegacyData(CampaignRegions::fromJson(entry.second["regions"]), entry.second["scenarioCount"].Integer());
+			if(!entry.second["loadingBackground"].isNull())
+				loadingBackground = ImagePath::builtin(entry.second["loadingBackground"].String());
+			if(!entry.second["introVideoRim"].isNull())
+				introVideoRim = ImagePath::builtin(entry.second["introVideoRim"].String());
+			if(!entry.second["introVideo"].isNull())
+				introVideo = VideoPath::builtin(entry.second["introVideo"].String());
+		}
+}
+
+void Campaign::overrideCampaignScenarios()
+{
+	const JsonNode node = JsonUtils::assembleFromFiles("config/campaignOverrides.json");
+	for (auto & entry : node.Struct())
+		if(filename == entry.first)
+		{
+			if(!entry.second["scenarios"].isNull())
+			{
+				auto sc = entry.second["scenarios"].Vector();
+				for(int i = 0; i < sc.size(); i++)
+				{
+					auto it = scenarios.begin();
+					std::advance(it, i);
+					if(!sc.at(i)["voiceProlog"].isNull())
+						it->second.prolog.prologVoice = AudioPath::builtin(sc.at(i)["voiceProlog"].String());
+					if(!sc.at(i)["voiceEpilog"].isNull())
+						it->second.epilog.prologVoice = AudioPath::builtin(sc.at(i)["voiceEpilog"].String());
+				}
+			}
+		}
+}
+
 int Campaign::scenariosCount() const
 {
 	return allScenarios().size();

+ 17 - 0
lib/campaign/CampaignState.h

@@ -83,6 +83,7 @@ public:
 class DLL_LINKAGE CampaignHeader : public boost::noncopyable
 {
 	friend class CampaignHandler;
+	friend class Campaign;
 
 	CampaignVersion version = CampaignVersion::NONE;
 	CampaignRegions campaignRegions;
@@ -96,11 +97,15 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable
 	std::string filename;
 	std::string modName;
 	std::string encoding;
+	ImagePath loadingBackground;
+	ImagePath introVideoRim;
+	VideoPath introVideo;
 
 	int numberOfScenarios = 0;
 	bool difficultyChosenByPlayer = false;
 
 	void loadLegacyData(ui8 campId);
+	void loadLegacyData(CampaignRegions regions, int numOfScenario);
 
 	TextContainerRegistrable textContainer;
 
@@ -118,6 +123,9 @@ public:
 	std::string getModName() const;
 	std::string getEncoding() const;
 	AudioPath getMusic() const;
+	ImagePath getLoadingBackground() const;
+	ImagePath getIntroVideoRim() const;
+	VideoPath getIntroVideo() const;
 
 	const CampaignRegions & getRegions() const;
 	TextContainerRegistrable & getTexts();
@@ -142,6 +150,12 @@ public:
 		h & music;
 		h & encoding;
 		h & textContainer;
+		if (h.version >= Handler::Version::CHRONICLES_SUPPORT)
+		{
+			h & loadingBackground;
+			h & introVideoRim;
+			h & introVideo;
+		}
 	}
 };
 
@@ -247,6 +261,9 @@ public:
 	std::set<CampaignScenarioID> allScenarios() const;
 	int scenariosCount() const;
 
+	void overrideCampaign();
+	void overrideCampaignScenarios();
+
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & static_cast<CampaignHeader&>(*this);

+ 11 - 6
lib/filesystem/CArchiveLoader.cpp

@@ -197,6 +197,11 @@ std::string CArchiveLoader::getMountPoint() const
 	return mountPoint;
 }
 
+const std::unordered_map<ResourcePath, ArchiveEntry> & CArchiveLoader::getEntries() const
+{
+	return entries;
+}
+
 std::unordered_set<ResourcePath> CArchiveLoader::getFilteredFiles(std::function<bool(const ResourcePath &)> filter) const
 {
 	std::unordered_set<ResourcePath> foundID;
@@ -209,7 +214,7 @@ std::unordered_set<ResourcePath> CArchiveLoader::getFilteredFiles(std::function<
 	return foundID;
 }
 
-void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry) const
+void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry, bool absolute) const
 {
 	si64 currentPosition = fileStream.tell(); // save filestream position
 
@@ -217,7 +222,7 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput
 	fileStream.seek(entry.offset);
 	fileStream.read(data.data(), entry.fullSize);
 
-	boost::filesystem::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name);
+	boost::filesystem::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name, absolute);
 
 	// writeToOutputFile
 	std::ofstream out(extractedFilePath.string(), std::ofstream::binary);
@@ -227,17 +232,17 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput
 	fileStream.seek(currentPosition); // restore filestream position
 }
 
-void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry) const
+void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry, bool absolute) const
 {
 	std::unique_ptr<CInputStream> inputStream = load(ResourcePath(mountPoint + entry.name));
 
 	entry.offset = 0;
-	extractToFolder(outputSubFolder, *inputStream, entry);
+	extractToFolder(outputSubFolder, *inputStream, entry, absolute);
 }
 
-boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName)
+boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName, bool absolute)
 {
-	boost::filesystem::path extractionFolderPath = VCMIDirs::get().userExtractedPath() / outputSubFolder;
+	boost::filesystem::path extractionFolderPath = absolute ? outputSubFolder : VCMIDirs::get().userExtractedPath() / outputSubFolder;
 	boost::filesystem::path extractedFilePath = extractionFolderPath / entryName;
 
 	boost::filesystem::create_directories(extractionFolderPath);

+ 4 - 3
lib/filesystem/CArchiveLoader.h

@@ -63,12 +63,13 @@ public:
 	std::unique_ptr<CInputStream> load(const ResourcePath & resourceName) const override;
 	bool existsResource(const ResourcePath & resourceName) const override;
 	std::string getMountPoint() const override;
+	const std::unordered_map<ResourcePath, ArchiveEntry> & getEntries() const;
 	void updateFilteredFiles(std::function<bool(const std::string &)> filter) const override {}
 	std::unordered_set<ResourcePath> getFilteredFiles(std::function<bool(const ResourcePath &)> filter) const override;
 	/** Extracts one archive entry to the specified subfolder. Used for Video and Sound */
-	void extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry) const;
+	void extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry, bool absolute = false) const;
 	/** Extracts one archive entry to the specified subfolder. Used for Images, Sprites, etc */
-	void extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry) const;
+	void extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry, bool absolute = false) const;
 
 private:
 	/**
@@ -105,6 +106,6 @@ private:
 };
 
 /** Constructs the file path for the extracted file. Creates the subfolder hierarchy aswell **/
-boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName);
+boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName, bool absolute);
 
 VCMI_LIB_NAMESPACE_END

+ 2 - 0
lib/mapping/CMapInfo.cpp

@@ -172,6 +172,8 @@ int CMapInfo::getMapSizeFormatIconId() const
 			return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["iconIndex"].Integer();
 		case EMapFormat::SOD:
 			return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["iconIndex"].Integer();
+		case EMapFormat::CHR:
+			return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES)["iconIndex"].Integer();
 		case EMapFormat::WOG:
 			return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["iconIndex"].Integer();
 		case EMapFormat::HOTA:

+ 1 - 0
lib/mapping/CMapService.cpp

@@ -157,6 +157,7 @@ std::unique_ptr<IMapLoader> CMapService::getMapLoader(std::unique_ptr<CInputStre
 			case static_cast<int>(EMapFormat::AB)  :
 			case static_cast<int>(EMapFormat::ROE) :
 			case static_cast<int>(EMapFormat::SOD) :
+			case static_cast<int>(EMapFormat::CHR) :
 			case static_cast<int>(EMapFormat::HOTA) :
 				return std::unique_ptr<IMapLoader>(new CMapLoaderH3M(mapName, modName, encoding, stream.get()));
 			default :

+ 12 - 0
lib/mapping/MapFeaturesH3M.cpp

@@ -25,6 +25,8 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::find(EMapFormat format, uint32_t hota
 			return getFeaturesAB();
 		case EMapFormat::SOD:
 			return getFeaturesSOD();
+		case EMapFormat::CHR:
+			return getFeaturesCHR();
 		case EMapFormat::WOG:
 			return getFeaturesWOG();
 		case EMapFormat::HOTA:
@@ -107,6 +109,16 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD()
 	return result;
 }
 
+MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesCHR()
+{
+	MapFormatFeaturesH3M result = getFeaturesSOD();
+	result.levelCHR = true;
+
+	result.heroesPortraitsCount = 169; // +6x tarnum
+
+	return result;
+}
+
 MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesWOG()
 {
 	MapFormatFeaturesH3M result = getFeaturesSOD();

+ 2 - 0
lib/mapping/MapFeaturesH3M.h

@@ -21,6 +21,7 @@ public:
 	static MapFormatFeaturesH3M getFeaturesROE();
 	static MapFormatFeaturesH3M getFeaturesAB();
 	static MapFormatFeaturesH3M getFeaturesSOD();
+	static MapFormatFeaturesH3M getFeaturesCHR();
 	static MapFormatFeaturesH3M getFeaturesWOG();
 	static MapFormatFeaturesH3M getFeaturesHOTA(uint32_t hotaVersion);
 
@@ -64,6 +65,7 @@ public:
 	bool levelROE = false;
 	bool levelAB = false;
 	bool levelSOD = false;
+	bool levelCHR = false;
 	bool levelWOG = false;
 	bool levelHOTA0 = false;
 	bool levelHOTA1 = false;

+ 1 - 1
lib/mapping/MapFormat.h

@@ -19,7 +19,7 @@ enum class EMapFormat : uint8_t
 	ROE   = 0x0e, // 14
 	AB    = 0x15, // 21
 	SOD   = 0x1c, // 28
-//	CHR   = 0x1d, // 29 Heroes Chronicles, presumably - identical to SoD, untested
+	CHR   = 0x1d, // 29
 	HOTA  = 0x20, // 32
 	WOG   = 0x33, // 51
 	VCMI  = 0x64

+ 3 - 0
lib/mapping/MapFormatH3M.cpp

@@ -135,6 +135,8 @@ static MapIdentifiersH3M generateMapping(EMapFormat format)
 		identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE));
 	if(features.levelSOD)
 		identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH));
+	if(features.levelCHR)
+		identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES));
 	if(features.levelWOG)
 		identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS));
 	if(features.levelHOTA0)
@@ -161,6 +163,7 @@ static std::map<EMapFormat, MapIdentifiersH3M> generateMappings()
 	addMapping(EMapFormat::ROE);
 	addMapping(EMapFormat::AB);
 	addMapping(EMapFormat::SOD);
+	addMapping(EMapFormat::CHR);
 	addMapping(EMapFormat::HOTA);
 	addMapping(EMapFormat::WOG);
 

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -56,6 +56,7 @@ enum class ESerializationVersion : int32_t
 	NEW_MARKETS, // 857 - reworked market classes
 	PLAYER_STATE_OWNED_OBJECTS, // 858 - player state stores all owned objects in a single list
 	SAVE_COMPATIBILITY_FIXES, // 859 - implementation of previoulsy postponed changes to serialization
+	CHRONICLES_SUPPORT, // 860 - support for heroes chronicles
 
-	CURRENT = SAVE_COMPATIBILITY_FIXES
+	CURRENT = CHRONICLES_SUPPORT
 };