diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 5d19ab960..2361608d0 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -532,7 +532,10 @@ void CServerHandler::sendGuiAction(ui8 action) const void CServerHandler::sendRestartGame() const { - GH.windows().createAndPushWindow(); + if(si->campState && !si->campState->getLoadingBackground().empty()) + GH.windows().createAndPushWindow(si->campState->getLoadingBackground()); + else + GH.windows().createAndPushWindow(); 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(); + { + if(si->campState && !si->campState->getLoadingBackground().empty()) + GH.windows().createAndPushWindow(si->campState->getLoadingBackground()); + else + GH.windows().createAndPushWindow(); + } LobbyPrepareStartGame lpsg; sendLobbyPack(lpsg); diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 72691458a..5d81fbdef 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/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(); - GH.windows().pushWindow(lobby->bonusSel); + if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty()) + GH.windows().createAndPushWindow(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) diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index d64847267..e6de278b1 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/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 bonusSel) + : CWindowObject(BORDERED), bonusSel(bonusSel) +{ + OBJECT_CONSTRUCTION; + + addUsedEvents(LCLICK | KEYBOARD); + + pos = center(Rect(0, 0, 800, 600)); + + videoPlayer = std::make_shared(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 CBonusSelection::getCampaign() { return CSH->si->campState; @@ -93,7 +129,9 @@ CBonusSelection::CBonusSelection() labelCampaignDescription = std::make_shared(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]); campaignDescription = std::make_shared(getCampaign()->getDescriptionTranslated(), Rect(480, 86, 286, 117), 1); - mapName = std::make_shared(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(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated(), availableSpace ); labelMapDescription = std::make_shared(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); mapDescription = std::make_shared("", Rect(480, 278, 292, 108), 1); diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index 989e459b3..e6b486390 100644 --- a/client/lobby/CBonusSelection.h +++ b/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 videoPlayer; + std::shared_ptr bonusSel; + + void exit(); +public: + CampaignIntroVideo(VideoPath video, ImagePath rim, std::shared_ptr 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 diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index a8111ed03..5e9e3fbc5 100644 --- a/client/lobby/SelectionTab.cpp +++ b/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: diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 90e6376d0..54cd0d7e0 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/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; diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 811dec13e..d7e9c5a1e 100644 --- a/client/mainmenu/CMainMenu.h +++ b/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; diff --git a/config/campaignOverrides.json b/config/campaignOverrides.json new file mode 100644 index 000000000..a14f4fa2f --- /dev/null +++ b/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" + } +} diff --git a/config/gameConfig.json b/config/gameConfig.json index 231185998..ea8d0b90a 100644 --- a/config/gameConfig.json +++ b/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 diff --git a/config/heroes/portraits.json b/config/heroes/portraits.json index d1fa147aa..aeecfb192 100644 --- a/config/heroes/portraits.json +++ b/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" : {} } } \ No newline at end of file diff --git a/docs/modders/Campaign_Format.md b/docs/modders/Campaign_Format.md index a6a085f1d..46cc7b2de 100644 --- a/docs/modders/Campaign_Format.md +++ b/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 diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index f128a9240..0eb22e284 100644 --- a/launcher/CMakeLists.txt +++ b/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 ) diff --git a/launcher/firstLaunch/firstlaunch_moc.cpp b/launcher/firstLaunch/firstlaunch_moc.cpp index 8f70abae7..b440dcdec 100644 --- a/launcher/firstLaunch/firstlaunch_moc.cpp +++ b/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); diff --git a/launcher/innoextract.cpp b/launcher/innoextract.cpp new file mode 100644 index 000000000..e4c40c00a --- /dev/null +++ b/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 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; +} diff --git a/launcher/innoextract.h b/launcher/innoextract.h new file mode 100644 index 000000000..4e5ee0a10 --- /dev/null +++ b/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 cb = nullptr); +}; diff --git a/launcher/modManager/chroniclesextractor.cpp b/launcher/modManager/chroniclesextractor.cpp new file mode 100644 index 000000000..6a4ff4f32 --- /dev/null +++ b/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 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(fileCount)) * static_cast(extractionFile)) + (progress / static_cast(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 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{"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{"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(); + } +} diff --git a/launcher/modManager/chroniclesextractor.h b/launcher/modManager/chroniclesextractor.h new file mode 100644 index 000000000..f9e1c1fc4 --- /dev/null +++ b/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 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 chronicles = { + {1, QByteArray{reinterpret_cast(u"Warlords of the Wasteland"), 50}}, + {2, QByteArray{reinterpret_cast(u"Conquest of the Underworld"), 52}}, + {3, QByteArray{reinterpret_cast(u"Masters of the Elements"), 46}}, + {4, QByteArray{reinterpret_cast(u"Clash of the Dragons"), 40}}, + {5, QByteArray{reinterpret_cast(u"The World Tree"), 28}}, + {6, QByteArray{reinterpret_cast(u"The Fiery Moon"), 28}}, + {7, QByteArray{reinterpret_cast(u"Revolt of the Beastmasters"), 52}}, + {8, QByteArray{reinterpret_cast(u"The Sword of Frost"), 36}} + }; +public: + void installChronicles(QStringList exe); + + ChroniclesExtractor(QWidget *p, std::function cb = nullptr); +}; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 5c351b366..63b17b3c3 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/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 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 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(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(); } diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 77c6b3c66..b10ddf504 100644 --- a/lib/GameSettings.cpp +++ b/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" }, diff --git a/lib/GameSettings.h b/lib/GameSettings.h index bcd8151f7..b1b4e8940 100644 --- a/lib/GameSettings.h +++ b/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, diff --git a/lib/campaign/CampaignConstants.h b/lib/campaign/CampaignConstants.h index e4a615d47..c380b77f0 100644 --- a/lib/campaign/CampaignConstants.h +++ b/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, diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index f1636e425..5c7110e00 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -38,12 +38,14 @@ void CampaignHandler::readCampaign(Campaign * ret, const std::vector & inpu CBinaryReader reader(&stream); readHeaderFromMemory(*ret, reader, filename, modName, encoding); + ret->overrideCampaign(); for(int g = 0; g < ret->numberOfScenarios; ++g) { auto scenarioID = static_cast(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(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(""); diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index d8236746a..f1fa305cb 100644 --- a/lib/campaign/CampaignState.cpp +++ b/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 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(); diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index 01f2b4bf4..303e7428e 100644 --- a/lib/campaign/CampaignState.h +++ b/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 allScenarios() const; int scenariosCount() const; + void overrideCampaign(); + void overrideCampaignScenarios(); + template void serialize(Handler &h) { h & static_cast(*this); diff --git a/lib/filesystem/CArchiveLoader.cpp b/lib/filesystem/CArchiveLoader.cpp index c7b47f59c..f0212d6ea 100644 --- a/lib/filesystem/CArchiveLoader.cpp +++ b/lib/filesystem/CArchiveLoader.cpp @@ -197,6 +197,11 @@ std::string CArchiveLoader::getMountPoint() const return mountPoint; } +const std::unordered_map & CArchiveLoader::getEntries() const +{ + return entries; +} + std::unordered_set CArchiveLoader::getFilteredFiles(std::function filter) const { std::unordered_set foundID; @@ -209,7 +214,7 @@ std::unordered_set 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 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); diff --git a/lib/filesystem/CArchiveLoader.h b/lib/filesystem/CArchiveLoader.h index 33b410062..d8797d7a5 100644 --- a/lib/filesystem/CArchiveLoader.h +++ b/lib/filesystem/CArchiveLoader.h @@ -63,12 +63,13 @@ public: std::unique_ptr load(const ResourcePath & resourceName) const override; bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; + const std::unordered_map & getEntries() const; void updateFilteredFiles(std::function filter) const override {} std::unordered_set getFilteredFiles(std::function 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 diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index 2100bcca8..58e37de88 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/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: diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index ec58cc174..2b7af0de3 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -157,6 +157,7 @@ std::unique_ptr CMapService::getMapLoader(std::unique_ptr(EMapFormat::AB) : case static_cast(EMapFormat::ROE) : case static_cast(EMapFormat::SOD) : + case static_cast(EMapFormat::CHR) : case static_cast(EMapFormat::HOTA) : return std::unique_ptr(new CMapLoaderH3M(mapName, modName, encoding, stream.get())); default : diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index 6b93a39f3..df4eb6c4c 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/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(); diff --git a/lib/mapping/MapFeaturesH3M.h b/lib/mapping/MapFeaturesH3M.h index d9fe3fc68..4768f0746 100644 --- a/lib/mapping/MapFeaturesH3M.h +++ b/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; diff --git a/lib/mapping/MapFormat.h b/lib/mapping/MapFormat.h index 8ae5b82f7..c96bc9894 100644 --- a/lib/mapping/MapFormat.h +++ b/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 diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index c04e124c3..9c54a16bc 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/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 generateMappings() addMapping(EMapFormat::ROE); addMapping(EMapFormat::AB); addMapping(EMapFormat::SOD); + addMapping(EMapFormat::CHR); addMapping(EMapFormat::HOTA); addMapping(EMapFormat::WOG); diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h index 7984bf7f6..5c72c10fa 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/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 };