浏览代码

campaign editor

Laserlicht 7 月之前
父节点
当前提交
28169b051e

+ 4 - 0
lib/campaign/CampaignState.cpp

@@ -431,6 +431,10 @@ std::unique_ptr<CMap> CampaignState::getMap(CampaignScenarioID scenarioId, IGame
 	std::string scenarioName = getFilename().substr(0, getFilename().find('.'));
 	boost::to_lower(scenarioName);
 	scenarioName += ':' + std::to_string(scenarioId.getNum());
+
+	if(!mapPieces.count(scenarioId) || !mapPieces.find(scenarioId)->second.size())
+		return nullptr;
+
 	const auto & mapContent = mapPieces.find(scenarioId)->second;
 	auto result = mapService.loadMap(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding(), cb);
 

+ 21 - 0
lib/campaign/CampaignState.h

@@ -32,6 +32,11 @@ class IGameCallback;
 
 class DLL_LINKAGE CampaignRegions
 {
+	// Campaign editor
+	friend class CampaignEditor;
+	friend class CampaignProperties;
+	friend class ScenarioProperties;
+
 	std::string campPrefix;
 	std::vector<std::string> campSuffix;
 	std::string campBackground;
@@ -85,6 +90,11 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable
 	friend class CampaignHandler;
 	friend class Campaign;
 
+	// Campaign editor
+	friend class CampaignEditor;
+	friend class CampaignProperties;
+	friend class ScenarioProperties;
+
 	CampaignVersion version = CampaignVersion::NONE;
 	CampaignRegions campaignRegions;
 	MetaString name;
@@ -251,6 +261,11 @@ class DLL_LINKAGE Campaign : public CampaignHeader, public Serializeable
 {
 	friend class CampaignHandler;
 
+	// Campaign editor
+	friend class CampaignEditor;
+	friend class CampaignProperties;
+	friend class ScenarioProperties;
+
 	std::map<CampaignScenarioID, CampaignScenario> scenarios;
 
 public:
@@ -273,6 +288,12 @@ public:
 class DLL_LINKAGE CampaignState : public Campaign
 {
 	friend class CampaignHandler;
+
+	// Campaign editor
+	friend class CampaignEditor;
+	friend class CampaignProperties;
+	friend class ScenarioProperties;
+
 	using ScenarioPoolType = std::vector<JsonNode>;
 	using CampaignPoolType = std::map<CampaignScenarioID, ScenarioPoolType>;
 	using GlobalPoolType = std::map<HeroTypeID, JsonNode>;

+ 6 - 0
lib/texts/MetaString.cpp

@@ -14,6 +14,7 @@
 #include "CCreatureHandler.h"
 #include "CCreatureSet.h"
 #include "entities/faction/CFaction.h"
+#include "entities/hero/CHero.h"
 #include "texts/CGeneralTextHandler.h"
 #include "CSkillHandler.h"
 #include "GameConstants.h"
@@ -350,6 +351,11 @@ void MetaString::appendName(const ArtifactID & id)
 	appendTextID(id.toEntity(LIBRARY)->getNameTextID());
 }
 
+void MetaString::appendName(const HeroTypeID & id)
+{
+	appendTextID(id.toEntity(LIBRARY)->getNameTextID());
+}
+
 void MetaString::appendName(const SpellID & id)
 {
 	appendTextID(id.toEntity(LIBRARY)->getNameTextID());

+ 2 - 0
lib/texts/MetaString.h

@@ -23,6 +23,7 @@ class SecondarySkill;
 class SpellID;
 class FactionID;
 class GameResID;
+class HeroTypeID;
 using TQuantity = si32;
 
 /// Strings classes that can be used as replacement in MetaString
@@ -78,6 +79,7 @@ public:
 	void appendNumber(int64_t value);
 
 	void appendName(const ArtifactID& id);
+	void appendName(const HeroTypeID& id);
 	void appendName(const SpellID& id);
 	void appendName(const PlayerColor& id);
 	void appendName(const CreatureID & id, TQuantity count);

+ 5 - 3
mapeditor/BitmapHandler.cpp

@@ -69,18 +69,20 @@ namespace BitmapHandler
 			it = (int)size - 256 * 3;
 			for(int i = 0; i < 256; i++)
 			{
-				char bytes[3];
+				unsigned char bytes[4];
 				bytes[0] = pcx[it++];
 				bytes[1] = pcx[it++];
 				bytes[2] = pcx[it++];
-				colorTable.append(qRgb(bytes[0], bytes[1], bytes[2]));
+				bytes[3] = (bytes[0] == 0 && bytes[1] == 255 && bytes[2] == 255) ? 0 : 255;
+
+				colorTable.append(qRgba(bytes[0], bytes[1], bytes[2], bytes[3]));
 			}
 			image.setColorTable(colorTable);
 			return image;
 		}
 		else
 		{
-			QImage image(pcx + it, width, height, width * 3, QImage::Format_RGB888);
+			QImage image(pcx + it, width, height, width * 3, QImage::Format_BGR888);
 			return image;
 		}
 	}

+ 16 - 0
mapeditor/CMakeLists.txt

@@ -41,6 +41,12 @@ set(editor_SRCS
 		inspector/PickObjectDelegate.cpp
 		inspector/portraitwidget.cpp
 		resourceExtractor/ResourceConverter.cpp
+		helper.cpp
+		campaigneditor/campaigneditor.cpp
+		campaigneditor/campaignproperties.cpp
+		campaigneditor/scenarioproperties.cpp
+		campaigneditor/startingbonus.cpp
+		campaigneditor/campaignview.cpp
 )
 
 set(editor_HEADERS
@@ -87,6 +93,12 @@ set(editor_HEADERS
 		inspector/baseinspectoritemdelegate.h
 		resourceExtractor/ResourceConverter.h
 		mapeditorroles.h
+		helper.h
+		campaigneditor/campaigneditor.h
+		campaigneditor/campaignproperties.h
+		campaigneditor/scenarioproperties.h
+		campaigneditor/startingbonus.h
+		campaigneditor/campaignview.h
 )
 
 set(editor_FORMS
@@ -118,6 +130,10 @@ set(editor_FORMS
 		inspector/heroskillswidget.ui
 		inspector/herospellwidget.ui
 		inspector/portraitwidget.ui
+		campaigneditor/campaigneditor.ui
+		campaigneditor/campaignproperties.ui
+		campaigneditor/scenarioproperties.ui
+		campaigneditor/startingbonus.ui
 )
 
 set(editor_RESOURCES

+ 1 - 0
mapeditor/StdInc.h

@@ -12,6 +12,7 @@
 #include "../Global.h"
 
 #define VCMI_EDITOR_NAME "VCMI Map Editor"
+#define VCMI_CAMP_EDITOR_NAME "VCMI Campaign Editor"
 
 #include <QtWidgets>
 #include <QStringList>

+ 251 - 0
mapeditor/campaigneditor/campaigneditor.cpp

@@ -0,0 +1,251 @@
+/*
+ * campaigneditor.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 "campaigneditor.h"
+#include "ui_campaigneditor.h"
+
+#include "campaignproperties.h"
+#include "scenarioproperties.h"
+
+#include "../BitmapHandler.h"
+#include "../helper.h"
+
+#include "../../lib/VCMIDirs.h"
+#include "../../lib/json/JsonNode.h"
+#include "../../lib/campaign/CampaignState.h"
+#include "../../lib/mapping/CMap.h"
+
+CampaignEditor::CampaignEditor():
+	ui(new Ui::CampaignEditor),
+	selectedScenario(CampaignScenarioID::NONE)
+{
+	ui->setupUi(this);
+	
+	setWindowIcon(QIcon{":/icons/menu-game.png"});
+	ui->actionOpen->setIcon(QIcon{":/icons/document-open.png"});
+	ui->actionSave->setIcon(QIcon{":/icons/document-save.png"});
+	ui->actionNew->setIcon(QIcon{":/icons/document-new.png"});
+	ui->actionScenarioProperties->setIcon(QIcon{":/icons/menu-settings.png"});
+	ui->actionCampaignProperties->setIcon(QIcon{":/icons/menu-mods.png"});
+
+	campaignScene.reset(new CampaignScene());
+	ui->campaignView->setScene(campaignScene.get());
+
+	redraw();
+
+	setTitle();
+	
+	setWindowModality(Qt::ApplicationModal);
+
+	show();
+}
+
+CampaignEditor::~CampaignEditor()
+{
+	delete ui;
+}
+
+void CampaignEditor::redraw()
+{
+	ui->actionSave->setEnabled(campaignState != nullptr);
+	ui->actionSave_as->setEnabled(campaignState != nullptr);
+	ui->actionScenarioProperties->setEnabled(campaignState != nullptr && campaignState->scenarios.count(selectedScenario));
+	ui->actionCampaignProperties->setEnabled(campaignState != nullptr);
+
+	if(!campaignState)
+		return;
+
+	campaignScene->clear();
+
+	auto background = BitmapHandler::loadBitmap(campaignState->getRegions().getBackgroundName().getName());
+	campaignScene->addItem(new QGraphicsPixmapItem(QPixmap::fromImage(background)));
+	for (auto & s : campaignState->scenarios)
+	{
+		auto scenario = s.first;
+		auto color = campaignState->scenarios.at(scenario).regionColor;
+		auto image = BitmapHandler::loadBitmap(campaignState->getRegions().getAvailableName(scenario, color).getName());
+		if(selectedScenario == scenario)
+			image = BitmapHandler::loadBitmap(campaignState->getRegions().getSelectedName(scenario, color).getName());
+		else if(campaignState->scenarios.at(scenario).mapName == "")
+			image = BitmapHandler::loadBitmap(campaignState->getRegions().getConqueredName(scenario, color).getName());
+		auto pixmap = new ClickablePixmapItem(QPixmap::fromImage(image), [this, scenario]()
+		{
+			bool redrawRequired = selectedScenario != scenario;
+			selectedScenario = scenario;
+
+			if(redrawRequired)
+				redraw();
+		}, [this, scenario]()
+		{
+			if(ScenarioProperties::showScenarioProperties(campaignState, scenario))
+				changed();
+			redraw();
+		}, [this, scenario](QGraphicsSceneContextMenuEvent * event)
+		{
+			QMenu contextMenu(this);
+			QAction *actionScenarioProperties = contextMenu.addAction(tr("Scenario editor"));
+			actionScenarioProperties->setIcon(ui->actionScenarioProperties->icon());
+			connect(actionScenarioProperties, &QAction::triggered, this, [this, scenario]() {
+				if(ScenarioProperties::showScenarioProperties(campaignState, scenario))
+					changed();
+				redraw();
+			});
+			contextMenu.exec(event->screenPos());
+		});
+		auto pos = campaignState->getRegions().getPosition(scenario);
+		pixmap->setPos(pos.x, pos.y);
+		pixmap->setToolTip(QString::fromStdString(campaignState->scenarios.at(scenario).mapName));
+		campaignScene->addItem(pixmap);
+	}
+
+	ui->campaignView->show();
+}
+
+bool CampaignEditor::getAnswerAboutUnsavedChanges()
+{
+	if(unsaved)
+	{
+		auto sure = QMessageBox::question(this, tr("Confirmation"), tr("Unsaved changes will be lost, are you sure?"));
+		if(sure == QMessageBox::No)
+		{
+			return false;
+		}
+	}
+	return true;
+}
+
+void CampaignEditor::setTitle()
+{
+	QFileInfo fileInfo(filename);
+	QString title = QString("%1%2 - %3 (%4)").arg(fileInfo.fileName(), unsaved ? "*" : "", VCMI_CAMP_EDITOR_NAME, GameConstants::VCMI_VERSION.c_str());
+	setWindowTitle(title);
+}
+
+void CampaignEditor::changed()
+{
+	unsaved = true;
+	setTitle();
+}
+
+bool CampaignEditor::saveCampaign()
+{
+	if(campaignState->mapPieces.size() != campaignState->campaignRegions.regions.size())
+	{
+		QMessageBox::critical(nullptr, tr("Maps missing"), tr("Not all Regions have a map. Please add them in Scenario Properties."));
+		return false;
+	}
+
+	Helper::saveCampaign(campaignState, filename);
+	return true;
+}
+
+void CampaignEditor::showCampaignEditor()
+{
+	auto * dialog = new CampaignEditor();
+
+	dialog->setAttribute(Qt::WA_DeleteOnClose);
+}
+
+void CampaignEditor::on_actionOpen_triggered()
+{
+	if(!getAnswerAboutUnsavedChanges())
+		return;
+	
+	auto filenameSelect = QFileDialog::getOpenFileName(this, tr("Open map"),
+		QString::fromStdString(VCMIDirs::get().userDataPath().make_preferred().string()),
+		tr("All supported campaigns (*.vcmp *.h3c);;VCMI campaigns(*.vcmp);;HoMM3 campaigns(*.h3c)"));
+	if(filenameSelect.isEmpty())
+		return;
+	
+	campaignState = Helper::openCampaignInternal(filenameSelect);
+	selectedScenario = *campaignState->allScenarios().begin();
+
+	redraw();
+}
+
+void CampaignEditor::on_actionSave_as_triggered()
+{
+	if(!campaignState)
+		return;
+
+	auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save campaign"), "", tr("VCMI campaigns (*.vcmp)"));
+
+	if(filenameSelect.isNull())
+		return;
+
+	QFileInfo fileInfo(filenameSelect);
+
+	if(fileInfo.suffix().toLower() != "vcmp")
+		filenameSelect += ".vcmp";
+
+	filename = filenameSelect;
+	if(saveCampaign())
+		unsaved = false;
+	setTitle();
+}
+
+void CampaignEditor::on_actionNew_triggered()
+{
+	if(!getAnswerAboutUnsavedChanges())
+		return;
+	
+	campaignState = std::make_unique<CampaignState>();
+	campaignState->campaignRegions = CampaignRegions::getLegacy(0);
+	for (int i = 0; i < campaignState->campaignRegions.regions.size(); i++)
+	{
+		CampaignScenario s;
+		s.travelOptions.startOptions = CampaignStartOptions::START_BONUS;
+		campaignState->scenarios.emplace(CampaignScenarioID(i), s);
+	}
+	campaignState->modName = "mapEditor";
+	
+	changed();
+	redraw();
+}
+
+void CampaignEditor::on_actionSave_triggered()
+{
+	if(!campaignState)
+		return;
+
+	if(filename.isNull())
+		on_actionSave_as_triggered();
+	else if(saveCampaign())
+		unsaved = false;
+	setTitle();
+}
+
+void CampaignEditor::on_actionCampaignProperties_triggered()
+{
+	if(!campaignState)
+		return;
+
+	if(CampaignProperties::showCampaignProperties(campaignState))
+		changed();
+	redraw();
+}
+
+void CampaignEditor::on_actionScenarioProperties_triggered()
+{
+	if(!campaignState || selectedScenario == CampaignScenarioID::NONE)
+		return;
+
+	if(ScenarioProperties::showScenarioProperties(campaignState, selectedScenario))
+		changed();
+	redraw();
+}
+
+void CampaignEditor::closeEvent(QCloseEvent *event)
+{
+	if(getAnswerAboutUnsavedChanges())
+		QDialog::closeEvent(event);
+	else
+		event->ignore();
+}

+ 60 - 0
mapeditor/campaigneditor/campaigneditor.h

@@ -0,0 +1,60 @@
+/*
+ * campaigneditor.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 <QDialog>
+
+#include "campaignview.h"
+
+#include "../StdInc.h"
+#include "../../lib/constants/EntityIdentifiers.h"
+
+class CampaignState;
+
+namespace Ui {
+class CampaignEditor;
+}
+
+class CampaignEditor : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit CampaignEditor();
+	~CampaignEditor();
+
+	void redraw();
+
+	static void showCampaignEditor();
+
+private slots:
+	void on_actionOpen_triggered();
+	void on_actionSave_as_triggered();
+	void on_actionNew_triggered();
+	void on_actionSave_triggered();
+	void on_actionCampaignProperties_triggered();
+	void on_actionScenarioProperties_triggered();
+	
+private:
+	bool getAnswerAboutUnsavedChanges();
+	void setTitle();
+	void changed();
+	bool saveCampaign();
+
+	void closeEvent(QCloseEvent *event) override;
+
+	Ui::CampaignEditor *ui;
+
+	std::unique_ptr<CampaignScene> campaignScene;
+
+	QString filename;
+	bool unsaved = false;
+	CampaignScenarioID selectedScenario;
+	std::shared_ptr<CampaignState> campaignState;
+};

+ 157 - 0
mapeditor/campaigneditor/campaigneditor.ui

@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CampaignEditor</class>
+ <widget class="QDialog" name="CampaignEditor">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>820</width>
+    <height>720</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>VCMI Campaign Editor</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <property name="Spacing" stdset="0">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QMenuBar" name="menubar">
+     <widget class="QMenu" name="menuFile">
+      <property name="title">
+       <string>File</string>
+      </property>
+      <addaction name="actionNew"/>
+      <addaction name="actionOpen"/>
+      <addaction name="actionSave"/>
+      <addaction name="actionSave_as"/>
+     </widget>
+     <widget class="QMenu" name="menuEdit">
+      <property name="title">
+       <string>Edit</string>
+      </property>
+      <addaction name="actionCampaignProperties"/>
+      <addaction name="actionScenarioProperties"/>
+     </widget>
+     <addaction name="menuFile"/>
+     <addaction name="menuEdit"/>
+    </widget>
+   </item>
+   <item>
+    <widget class="QToolBar" name="toolBar">
+     <property name="windowTitle">
+      <string>Toolbar</string>
+     </property>
+     <property name="bottomMargin" stdset="0">
+      <number>0</number>
+     </property>
+     <property name="topMargin" stdset="0">
+      <number>0</number>
+     </property>
+     <addaction name="actionNew"/>
+     <addaction name="actionOpen"/>
+     <addaction name="actionSave"/>
+     <addaction name="separator"/>
+     <addaction name="actionCampaignProperties"/>
+     <addaction name="actionScenarioProperties"/>
+    </widget>
+   </item>
+   <item>
+    <widget class="CampaignView" name="campaignView">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="mouseTracking">
+      <bool>true</bool>
+     </property>
+     <property name="sizeAdjustPolicy">
+      <enum>QAbstractScrollArea::AdjustToContents</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QStatusBar" name="statusbar"/>
+   </item>
+  </layout>
+  <action name="actionOpen">
+   <property name="text">
+    <string>Open</string>
+   </property>
+   <property name="shortcut">
+    <string notr="true">Ctrl+O</string>
+   </property>
+  </action>
+  <action name="actionSave">
+   <property name="text">
+    <string>Save</string>
+   </property>
+   <property name="shortcut">
+    <string notr="true">Ctrl+S</string>
+   </property>
+  </action>
+  <action name="actionNew">
+   <property name="text">
+    <string>New</string>
+   </property>
+   <property name="shortcut">
+    <string notr="true">Ctrl+N</string>
+   </property>
+  </action>
+  <action name="actionSave_as">
+   <property name="text">
+    <string>Save as...</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+Shift+S</string>
+   </property>
+  </action>
+  <action name="actionCampaignProperties">
+   <property name="text">
+    <string>Campaign Properties</string>
+   </property>
+   <property name="toolTip">
+    <string>Campaign Properties</string>
+   </property>
+   <property name="shortcut">
+    <string notr="true">Ctrl+Enter</string>
+   </property>
+  </action>
+  <action name="actionScenarioProperties">
+   <property name="text">
+    <string>Scenario Properties</string>
+   </property>
+   <property name="toolTip">
+    <string>Scenario Properties</string>
+   </property>
+   <property name="shortcut">
+    <string notr="true">Enter</string>
+   </property>
+  </action>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>CampaignView</class>
+   <extends>QGraphicsView</extends>
+   <header>campaigneditor/campaignview.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>

+ 193 - 0
mapeditor/campaigneditor/campaignproperties.cpp

@@ -0,0 +1,193 @@
+/*
+ * campaignproperties.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 "campaignproperties.h"
+#include "ui_campaignproperties.h"
+
+#include "../../lib/GameLibrary.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+#include "../../lib/campaign/CampaignState.h"
+#include "../../lib/constants/StringConstants.h"
+
+CampaignProperties::CampaignProperties(std::shared_ptr<CampaignState> campaignState):
+	ui(new Ui::CampaignProperties),
+	campaignState(campaignState),
+	regions(campaignState->campaignRegions)
+{
+	ui->setupUi(this);
+
+	setWindowTitle(tr("Campaign Properties"));
+	
+	setWindowModality(Qt::ApplicationModal);
+
+	ui->lineEditName->setText(QString::fromStdString(campaignState->name.toString()));
+	ui->textEditDescription->setText(QString::fromStdString(campaignState->description.toString()));
+	ui->lineEditAuthor->setText(QString::fromStdString(campaignState->author.toString()));
+	ui->lineEditAuthorContact->setText(QString::fromStdString(campaignState->authorContact.toString()));
+	ui->dateTimeEditCreationDateTime->setDateTime(QDateTime::fromSecsSinceEpoch(campaignState->creationDateTime));
+	ui->lineEditCampaignVersion->setText(QString::fromStdString(campaignState->campaignVersion.toString()));
+	ui->lineEditMusic->setText(QString::fromStdString(campaignState->music.getName()));
+	ui->checkBoxScenarioDifficulty->setChecked(campaignState->difficultyChosenByPlayer);
+	
+	for (int i = 0; i < 20; i++)
+		ui->comboBoxRegionPreset->insertItem(i, QString::fromStdString(LIBRARY->generaltexth->translate("core.camptext.names", i)));
+	ui->comboBoxRegionPreset->insertItem(20, tr("Custom"));
+	ui->comboBoxRegionPreset->setCurrentIndex(20);
+
+	loadRegion();
+
+	show();
+}
+
+CampaignProperties::~CampaignProperties()
+{
+	delete ui;
+}
+
+bool CampaignProperties::showCampaignProperties(std::shared_ptr<CampaignState> campaignState)
+{
+	if(!campaignState)
+		return false;
+
+	auto * dialog = new CampaignProperties(campaignState);
+
+	dialog->setAttribute(Qt::WA_DeleteOnClose);
+
+	return dialog->exec() == QDialog::Accepted;
+}
+
+void CampaignProperties::on_buttonBox_clicked(QAbstractButton * button)
+{
+	if(button == ui->buttonBox->button(QDialogButtonBox::Ok))
+	{
+		if(!saveRegion())
+			return;
+		campaignState->name = MetaString::createFromRawString(ui->lineEditName->text().toStdString());
+		campaignState->description = MetaString::createFromRawString(ui->textEditDescription->toPlainText().toStdString());
+		campaignState->author = MetaString::createFromRawString(ui->lineEditAuthor->text().toStdString());
+		campaignState->authorContact = MetaString::createFromRawString(ui->lineEditAuthorContact->text().toStdString());
+		campaignState->creationDateTime = ui->dateTimeEditCreationDateTime->dateTime().toSecsSinceEpoch();
+		campaignState->campaignVersion = MetaString::createFromRawString(ui->lineEditCampaignVersion->text().toStdString());
+		campaignState->music = AudioPath::builtin(ui->lineEditMusic->text().toStdString());
+		campaignState->difficultyChosenByPlayer = ui->checkBoxScenarioDifficulty->isChecked();
+		accept();
+	}
+	close();
+}
+
+void CampaignProperties::on_comboBoxRegionPreset_currentIndexChanged(int index)
+{
+	if(ui->comboBoxRegionPreset->count() == 21 && ui->comboBoxRegionPreset->currentIndex() != 20)
+		regions = CampaignRegions::getLegacy(ui->comboBoxRegionPreset->currentIndex());
+	
+	loadRegion();
+}
+
+void CampaignProperties::on_pushButtonRegionAdd_clicked()
+{
+	int row = ui->tableWidgetRegions->rowCount();
+	ui->tableWidgetRegions->insertRow(row);
+	ui->tableWidgetRegions->setItem(row, 0, new QTableWidgetItem("INFIX"));
+	ui->tableWidgetRegions->setItem(row, 1, new QTableWidgetItem(QString::number(0)));
+	ui->tableWidgetRegions->setItem(row, 2, new QTableWidgetItem(QString::number(0)));
+	ui->tableWidgetRegions->setItem(row, 3, new QTableWidgetItem(QString::number(-1)));
+	ui->tableWidgetRegions->setItem(row, 4, new QTableWidgetItem(QString::number(-1)));
+}
+
+void CampaignProperties::on_pushButtonRegionRemove_clicked()
+{
+	int rows = ui->tableWidgetRegions->rowCount() - 1;
+	ui->tableWidgetRegions->removeRow(rows);
+	ui->tableWidgetRegions->setRowCount(rows);
+}
+
+void CampaignProperties::loadRegion()
+{
+	ui->lineEditBackground->setText(QString::fromStdString(regions.campBackground.empty() ? regions.campPrefix + "_BG" : regions.campBackground));
+	ui->lineEditSuffix1->setText(QString::fromStdString(regions.campSuffix.size() ? regions.campSuffix[0] : "En"));
+	ui->lineEditSuffix2->setText(QString::fromStdString(regions.campSuffix.size() ? regions.campSuffix[1] : "Se"));
+	ui->lineEditSuffix3->setText(QString::fromStdString(regions.campSuffix.size() ? regions.campSuffix[2] : "Co"));
+	ui->lineEditPrefix->setText(QString::fromStdString(regions.campPrefix));
+	ui->spinBoxColorSuffixLength->setValue(regions.colorSuffixLength);
+
+	ui->tableWidgetRegions->clearContents();
+	ui->tableWidgetRegions->setRowCount(0);
+	ui->tableWidgetRegions->setColumnCount(5);
+	ui->tableWidgetRegions->setHorizontalHeaderLabels({tr("Infix"), tr("X"), tr("Y"), tr("Label Pos X"), tr("Label Pos Y")});
+	for (int i = 0; i < regions.regions.size(); ++i)
+	{
+		ui->tableWidgetRegions->insertRow(ui->tableWidgetRegions->rowCount());
+		ui->tableWidgetRegions->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(regions.regions[i].infix)));
+		ui->tableWidgetRegions->setItem(i, 1, new QTableWidgetItem(QString::number(regions.regions[i].pos.x)));
+		ui->tableWidgetRegions->setItem(i, 2, new QTableWidgetItem(QString::number(regions.regions[i].pos.y)));
+		ui->tableWidgetRegions->setItem(i, 3, new QTableWidgetItem(QString::number(regions.regions[i].labelPos.has_value() ? (*regions.regions[i].labelPos).x : -1)));
+		ui->tableWidgetRegions->setItem(i, 4, new QTableWidgetItem(QString::number(regions.regions[i].labelPos.has_value() ? (*regions.regions[i].labelPos).y : -1)));
+	}
+	ui->tableWidgetRegions->resizeColumnsToContents();
+}
+
+bool CampaignProperties::saveRegion()
+{
+	regions.campBackground = ui->lineEditBackground->text().toStdString();
+	if(regions.campSuffix.size() == 3)
+	{
+		regions.campSuffix[0] = ui->lineEditSuffix1->text().toStdString();
+		regions.campSuffix[1] = ui->lineEditSuffix2->text().toStdString();
+		regions.campSuffix[2] = ui->lineEditSuffix3->text().toStdString();
+	}
+	else
+	{
+		regions.campSuffix.push_back(ui->lineEditSuffix1->text().toStdString());
+		regions.campSuffix.push_back(ui->lineEditSuffix2->text().toStdString());
+		regions.campSuffix.push_back(ui->lineEditSuffix3->text().toStdString());
+	}
+	regions.campPrefix = ui->lineEditPrefix->text().toStdString();
+	regions.colorSuffixLength = ui->spinBoxColorSuffixLength->value();
+
+	regions.regions.clear();
+	for (int i = 0; i < ui->tableWidgetRegions->rowCount(); ++i)
+	{
+		CampaignRegions::RegionDescription rd;
+
+		rd.infix = ui->tableWidgetRegions->item(i, 0)->text().toStdString();
+		rd.pos.x = ui->tableWidgetRegions->item(i, 1)->text().toInt();
+		rd.pos.y = ui->tableWidgetRegions->item(i, 2)->text().toInt();
+		auto labelX = ui->tableWidgetRegions->item(i, 3)->text().toInt();
+		auto labelY = ui->tableWidgetRegions->item(i, 4)->text().toInt();
+		if(labelX == -1 || labelY == -1)
+			rd.labelPos = std::nullopt;
+		else
+			Point(labelX, labelY);
+
+		regions.regions.push_back(rd);
+	}
+
+	if(campaignState->campaignRegions.regions.size() > regions.regions.size())
+	{
+		QMessageBox::StandardButton reply;
+		reply = QMessageBox::question(this, tr("Fewer Scenarios"), tr("New Region setup supports fewer scenarios than before. Some will removed. Continue?"), QMessageBox::Yes|QMessageBox::No);
+		if (reply != QMessageBox::Yes)
+			return false;
+	}
+
+	campaignState->campaignRegions = regions;
+
+	while(campaignState->scenarios.size() < campaignState->campaignRegions.regions.size())
+		campaignState->scenarios.emplace(CampaignScenarioID(std::prev(campaignState->scenarios.end())->first + 1), CampaignScenario());
+	while(campaignState->scenarios.size() > campaignState->campaignRegions.regions.size())
+	{
+		auto elem = std::prev(campaignState->scenarios.end());
+		campaignState->mapPieces.erase(elem->first);
+		campaignState->scenarios.erase(elem);
+	}
+		
+	
+	return true;
+}

+ 46 - 0
mapeditor/campaigneditor/campaignproperties.h

@@ -0,0 +1,46 @@
+/*
+ * campaignproperties.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 <QDialog>
+
+#include "../../lib/campaign/CampaignState.h"
+
+class CampaignState;
+class QAbstractButton;
+
+namespace Ui {
+class CampaignProperties;
+}
+
+class CampaignProperties : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit CampaignProperties(std::shared_ptr<CampaignState> campaignState);
+	~CampaignProperties();
+
+	static bool showCampaignProperties(std::shared_ptr<CampaignState> campaignState);
+	
+private slots:
+	void on_buttonBox_clicked(QAbstractButton * button);
+	void on_comboBoxRegionPreset_currentIndexChanged(int index);
+	void on_pushButtonRegionAdd_clicked();
+	void on_pushButtonRegionRemove_clicked();
+
+private:
+	Ui::CampaignProperties *ui;
+
+	std::shared_ptr<CampaignState> campaignState;
+	CampaignRegions regions;
+
+	void loadRegion();
+	bool saveRegion();
+};

+ 332 - 0
mapeditor/campaigneditor/campaignproperties.ui

@@ -0,0 +1,332 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CampaignProperties</class>
+ <widget class="QDialog" name="CampaignProperties">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>499</width>
+    <height>580</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Campaign Properties</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tabGeneral">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <attribute name="title">
+       <string>General</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <item>
+        <widget class="QLabel" name="labelName">
+         <property name="text">
+          <string>Campaign name</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditName"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelDescription">
+         <property name="text">
+          <string>Campaign description</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QTextEdit" name="textEditDescription"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelAuthor">
+         <property name="text">
+          <string>Author</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditAuthor"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelAuthorContact">
+         <property name="text">
+          <string>Author contact (e.g. e-mail)</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditAuthorContact"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelCreationDateTime">
+         <property name="text">
+          <string>Campaign creation date</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QDateTimeEdit" name="dateTimeEditCreationDateTime"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelCampaignVersion">
+         <property name="text">
+          <string>Campaign version</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditCampaignVersion"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelMusic">
+         <property name="text">
+          <string>Music</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditMusic"/>
+       </item>
+       <item>
+        <widget class="QCheckBox" name="checkBoxScenarioDifficulty">
+         <property name="text">
+          <string>Scenario difficulty is user selectable</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabRegions">
+      <attribute name="title">
+       <string>Regions</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QLabel" name="labelRegionPreset">
+         <property name="text">
+          <string>Regions Preset</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxRegionPreset"/>
+       </item>
+       <item>
+        <widget class="Line" name="line">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelBackground">
+         <property name="text">
+          <string>Background</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditBackground"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelSuffix">
+         <property name="text">
+          <string>Suffix</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <widget class="QLineEdit" name="lineEditSuffix1"/>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="lineEditSuffix2"/>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="lineEditSuffix3"/>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelPrefix">
+         <property name="text">
+          <string>Prefix</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditPrefix"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelColorSuffixLength">
+         <property name="text">
+          <string>Color suffix length</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QSpinBox" name="spinBoxColorSuffixLength">
+         <property name="maximum">
+          <number>2</number>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelRegions">
+         <property name="text">
+          <string>Regions</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <widget class="QTableWidget" name="tableWidgetRegions"/>
+         </item>
+         <item>
+          <layout class="QVBoxLayout" name="verticalLayout_5">
+           <property name="leftMargin">
+            <number>0</number>
+           </property>
+           <property name="topMargin">
+            <number>0</number>
+           </property>
+           <item>
+            <widget class="QPushButton" name="pushButtonRegionAdd">
+             <property name="text">
+              <string>Add</string>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <widget class="QPushButton" name="pushButtonRegionRemove">
+             <property name="text">
+              <string>Remove</string>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <spacer name="verticalSpacer">
+             <property name="orientation">
+              <enum>Qt::Vertical</enum>
+             </property>
+             <property name="sizeHint" stdset="0">
+              <size>
+               <width>20</width>
+               <height>40</height>
+              </size>
+             </property>
+            </spacer>
+           </item>
+          </layout>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabMisc">
+      <attribute name="title">
+       <string>Misc</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_4">
+       <item>
+        <widget class="QLabel" name="labelLoadingBackground">
+         <property name="text">
+          <string>Loading background image</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditLoadingBackground"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelVideoRim">
+         <property name="text">
+          <string>Video rim image</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditVideoRim"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelIntroVideo">
+         <property name="text">
+          <string>Intro video</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditIntroVideo"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelOutroVideo">
+         <property name="text">
+          <string>Outro video</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditOutroVideo"/>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 48 - 0
mapeditor/campaigneditor/campaignview.cpp

@@ -0,0 +1,48 @@
+/*
+ * campaignview.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 "campaignview.h"
+
+CampaignScene::CampaignScene():
+	QGraphicsScene(nullptr)
+{
+}
+
+CampaignView::CampaignView(QWidget * parent):
+	QGraphicsView(parent)
+{
+}
+
+ClickablePixmapItem::ClickablePixmapItem(const QPixmap &pixmap, std::function<void()> clickedCallback, std::function<void()> doubleClickedCallback, std::function<void(QGraphicsSceneContextMenuEvent *)> contextMenuCallback):
+	QGraphicsPixmapItem(pixmap),
+	clickedCallback(clickedCallback),
+	doubleClickedCallback(doubleClickedCallback),
+	contextMenuCallback(contextMenuCallback)
+{
+}
+
+void ClickablePixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
+{
+	if(clickedCallback)
+		clickedCallback();
+}
+
+void ClickablePixmapItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
+{
+	if(doubleClickedCallback)
+		doubleClickedCallback();
+}
+
+void ClickablePixmapItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
+{
+	if(contextMenuCallback)
+		contextMenuCallback(event);
+}

+ 44 - 0
mapeditor/campaigneditor/campaignview.h

@@ -0,0 +1,44 @@
+/*
+ * campaignview.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 <QGraphicsView>
+#include <QGraphicsPixmapItem>
+
+class CampaignScene : public QGraphicsScene
+{
+	Q_OBJECT;
+public:
+	CampaignScene();
+};
+
+class CampaignView : public QGraphicsView
+{
+	Q_OBJECT
+
+public:
+	CampaignView(QWidget * parent);
+};
+
+class ClickablePixmapItem : public QGraphicsPixmapItem {
+public:
+	ClickablePixmapItem(const QPixmap &pixmap, std::function<void()> clickedCallback, std::function<void()> doubleClickedCallback, std::function<void(QGraphicsSceneContextMenuEvent *)> contextMenuCallback);
+
+protected:
+	void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
+	void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
+	void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
+
+private:
+	std::function<void()> clickedCallback;
+	std::function<void()> doubleClickedCallback;
+	std::function<void(QGraphicsSceneContextMenuEvent *)> contextMenuCallback;
+};

+ 529 - 0
mapeditor/campaigneditor/scenarioproperties.cpp

@@ -0,0 +1,529 @@
+/*
+ * scenarioproperties.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 "scenarioproperties.h"
+#include "ui_scenarioproperties.h"
+#include "startingbonus.h"
+
+#include "../../lib/GameLibrary.h"
+#include "../../lib/CArtHandler.h"
+#include "../../lib/CCreatureHandler.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+#include "../../lib/entities/hero/CHeroHandler.h"
+#include "../../lib/campaign/CampaignState.h"
+#include "../../lib/mapping/CMap.h"
+#include "../../lib/constants/StringConstants.h"
+
+ScenarioProperties::ScenarioProperties(std::shared_ptr<CampaignState> campaignState, CampaignScenarioID scenario):
+	ui(new Ui::ScenarioProperties),
+	campaignState(campaignState),
+	map(campaignState->getMap(scenario, nullptr)),
+	scenario(scenario)
+{
+	ui->setupUi(this);
+
+	setWindowTitle(tr("Scenario Properties"));
+	
+	setWindowModality(Qt::ApplicationModal);
+
+	ui->lineEditRegionName->setText(getRegionChar(scenario.getNum()));
+	ui->plainTextEditRightClickText->setPlainText(QString::fromStdString(campaignState->scenarios.at(scenario).regionText.toString()));
+	
+	for(int i = 0, index = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
+	{
+		MetaString str;
+		str.appendName(PlayerColor(i));
+		ui->comboBoxRegionColor->addItem(QString::fromStdString(str.toString()), QVariant(i));
+		if(i == campaignState->scenarios.at(scenario).regionColor)
+			ui->comboBoxRegionColor->setCurrentIndex(index);
+		++index;
+	}
+
+	for(int i = 0, index = 0; i < 5; ++i)
+	{
+		ui->comboBoxDefaultDifficulty->addItem(QString::fromStdString(LIBRARY->generaltexth->arraytxt[142 + i]), QVariant(i));
+		if(i == campaignState->scenarios.at(scenario).difficulty)
+			ui->comboBoxDefaultDifficulty->setCurrentIndex(index);
+		++index;
+	}
+
+	for(int i = 0; i < scenario.getNum(); ++i)
+	{
+		auto tmpScenario = CampaignScenarioID(i);
+		auto * item = new QListWidgetItem(getRegionChar(tmpScenario.getNum()) + " - " + QString::fromStdString(campaignState->scenarios.at(tmpScenario).mapName));
+		item->setData(Qt::UserRole, QVariant::fromValue(i));
+		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+		item->setCheckState(campaignState->scenarios.at(scenario).preconditionRegions.count(tmpScenario) ? Qt::Checked : Qt::Unchecked);
+		ui->listWidgetPrerequisites->addItem(item);
+	}
+
+	ui->checkBoxPrologueEnabled->setChecked(campaignState->scenarios.at(scenario).prolog.hasPrologEpilog);
+	ui->lineEditPrologueVideo->setText(QString::fromStdString(campaignState->scenarios.at(scenario).prolog.prologVideo.getName()));
+	ui->lineEditPrologueMusic->setText(QString::fromStdString(campaignState->scenarios.at(scenario).prolog.prologMusic.getName()));
+	ui->lineEditPrologueVoice->setText(QString::fromStdString(campaignState->scenarios.at(scenario).prolog.prologVoice.getName()));
+	ui->plainTextEditPrologueText->setPlainText(QString::fromStdString(campaignState->scenarios.at(scenario).prolog.prologText.toString()));
+	ui->checkBoxEpilogueEnabled->setChecked(campaignState->scenarios.at(scenario).epilog.hasPrologEpilog);
+	ui->lineEditEpilogueVideo->setText(QString::fromStdString(campaignState->scenarios.at(scenario).epilog.prologVideo.getName()));
+	ui->lineEditEpilogueMusic->setText(QString::fromStdString(campaignState->scenarios.at(scenario).epilog.prologMusic.getName()));
+	ui->lineEditEpilogueVoice->setText(QString::fromStdString(campaignState->scenarios.at(scenario).epilog.prologVoice.getName()));
+	ui->plainTextEditEpilogueText->setPlainText(QString::fromStdString(campaignState->scenarios.at(scenario).epilog.prologText.toString()));
+
+	ui->checkBoxCrossoverExperience->setChecked(campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.experience);
+	ui->checkBoxCrossoverPrimarySkills->setChecked(campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.primarySkills);
+	ui->checkBoxCrossoverSecondarySkills->setChecked(campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.secondarySkills);
+	ui->checkBoxCrossoverSpells->setChecked(campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.spells);
+	ui->checkBoxCrossoverArtifacts->setChecked(campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.artifacts);
+
+	ui->radioButtonStartingOptionBonus->setChecked(campaignState->scenarios.at(scenario).travelOptions.startOptions == CampaignStartOptions::START_BONUS);
+	ui->radioButtonStartingOptionHeroCrossover->setChecked(campaignState->scenarios.at(scenario).travelOptions.startOptions == CampaignStartOptions::HERO_CROSSOVER);
+	ui->radioButtonStartingOptionStartingHero->setChecked(campaignState->scenarios.at(scenario).travelOptions.startOptions == CampaignStartOptions::HERO_OPTIONS);
+
+	for(auto const & objectPtr : LIBRARY->arth->objects)
+	{
+		auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated()));
+		item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex()));
+		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+		item->setCheckState(campaignState->scenarios.at(scenario).travelOptions.artifactsKeptByHero.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked);
+		ui->listWidgetCrossoverArtifacts->addItem(item);
+	}
+	
+	for(auto const & objectPtr : LIBRARY->creh->objects)
+	{
+		auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameSingularTranslated()));
+		item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex()));
+		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+		item->setCheckState(campaignState->scenarios.at(scenario).travelOptions.monstersKeptByHero.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked);
+		ui->listWidgetCrossoverCreatures->addItem(item);
+	}
+
+	ui->tabWidgetStartOptions->tabBar()->hide();
+	ui->tabWidgetStartOptions->setStyleSheet("QTabWidget::pane { border: 0; }");
+
+	ui->tableWidgetStartingCrossover->setColumnCount(2);
+
+	LIBRARY->heroTypes()->forEach([this](const HeroType * hero, bool &)
+	{
+		heroSelection.emplace(hero->getId().getNum(), hero->getNameTranslated());
+	});
+	heroSelection.emplace(0xFFFD, tr("Strongest").toStdString());
+	heroSelection.emplace(0xFFFE, tr("Generated").toStdString());
+	heroSelection.emplace(0xFFFF, tr("Random").toStdString());
+
+	reloadMapRelatedUi();
+
+	show();
+}
+
+ScenarioProperties::~ScenarioProperties()
+{
+	delete ui;
+}
+
+void ScenarioProperties::reloadMapRelatedUi()
+{
+	map = campaignState->getMap(scenario, nullptr);
+
+	ui->lineEditMapFile->setText(QString::fromStdString(campaignState->scenarios.at(scenario).mapName));
+	ui->lineEditScenarioName->setText(map ? QString::fromStdString(map->name.toString()) : tr("No map"));
+	if(!map)
+		ui->lineEditScenarioName->setStyleSheet("background-color: red;");
+	else
+		ui->lineEditScenarioName->setStyleSheet(""); 
+
+	ui->pushButtonExport->setEnabled(map != nullptr);
+	ui->pushButtonRemove->setEnabled(map != nullptr);
+
+	ui->radioButtonStartingOptionHeroCrossover->setEnabled(scenario.getNum() > 0);
+	bool allowSelectingStartHero = false;
+	if(map != nullptr)
+		for(auto & player : map->players)
+			if(player.generateHeroAtMainTown)
+				allowSelectingStartHero = true;
+	ui->radioButtonStartingOptionStartingHero->setEnabled(allowSelectingStartHero);
+
+	ui->tabPrologueEpilogue->setEnabled(map != nullptr);
+	ui->tabCrossover->setEnabled(map != nullptr);
+	ui->tabStarting->setEnabled(map != nullptr);
+
+	ui->comboBoxDefaultDifficulty->setEnabled(map != nullptr);
+	ui->listWidgetPrerequisites->setEnabled(map != nullptr);
+	ui->plainTextEditRightClickText->setEnabled(map != nullptr);
+
+	ui->tableWidgetStartingCrossover->clearContents();
+	ui->tableWidgetStartingCrossover->setRowCount(0);
+	ui->listWidgetStartingBonusOption->clear();
+
+	if(map != nullptr)
+	{
+		std::vector<PlayerColor> selectableColors;
+		for(int i = 0; i < map->players.size(); i++)
+			if(map->players[i].canHumanPlay)
+				selectableColors.push_back(PlayerColor(i));
+
+		ui->comboBoxStartingBonusPlayerPosition->clear();
+		for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
+		{
+			if(!vstd::contains(selectableColors, PlayerColor(i)))
+				continue;
+
+			MetaString str;
+			str.appendRawString((tr("Player") + " ").toStdString());
+			str.appendNumber(i + 1);
+			str.appendRawString(" (");
+			str.appendName(PlayerColor(i));
+			str.appendRawString(")");
+			ui->comboBoxStartingBonusPlayerPosition->addItem(QString::fromStdString(str.toString()), QVariant(i));
+		}
+		int index = ui->comboBoxStartingBonusPlayerPosition->findData(campaignState->scenarios.at(scenario).travelOptions.playerColor.getNum());
+		if(index != -1)
+			ui->comboBoxStartingBonusPlayerPosition->setCurrentIndex(index);
+	}
+
+	if(campaignState->scenarios.at(scenario).travelOptions.startOptions == CampaignStartOptions::HERO_CROSSOVER || campaignState->scenarios.at(scenario).travelOptions.startOptions == CampaignStartOptions::HERO_OPTIONS)
+	{
+		if(campaignState->scenarios.at(scenario).travelOptions.startOptions == CampaignStartOptions::HERO_OPTIONS)
+			on_radioButtonStartingOptionStartingHero_toggled();
+		else
+			on_radioButtonStartingOptionHeroCrossover_toggled();
+
+		for(int i = 0; i < campaignState->scenarios.at(scenario).travelOptions.bonusesToChoose.size(); i++)
+		{
+			auto bonus = campaignState->scenarios.at(scenario).travelOptions.bonusesToChoose[i];
+
+			ui->tableWidgetStartingCrossover->insertRow(i);
+			QComboBox* comboBoxOption = new QComboBox();
+			if(campaignState->scenarios.at(scenario).travelOptions.startOptions == CampaignStartOptions::HERO_OPTIONS)
+			{
+				for(auto & hero : heroSelection)
+					comboBoxOption->addItem(QString::fromStdString(hero.second), hero.first);
+			}
+			else
+			{
+				for(int i = 0; i < scenario.getNum(); ++i)
+				{
+					auto tmpScenario = CampaignScenarioID(i);
+					auto text = getRegionChar(tmpScenario.getNum()) + " - " + QString::fromStdString(campaignState->scenarios.at(tmpScenario).mapName);
+					comboBoxOption->addItem(text, i);
+				}
+			}
+
+			QComboBox* comboBoxPlayer = new QComboBox();
+			for(int i = 0; i < ui->comboBoxStartingBonusPlayerPosition->count(); ++i) // copy from player dropdown
+				comboBoxPlayer->addItem(ui->comboBoxStartingBonusPlayerPosition->itemText(i), ui->comboBoxStartingBonusPlayerPosition->itemData(i));
+
+			// set selected
+			int index = comboBoxPlayer->findData(bonus.info1);
+			if(index != -1)
+				comboBoxPlayer->setCurrentIndex(index);
+			index = comboBoxOption->findData(bonus.info2);
+			if(index != -1)
+				comboBoxOption->setCurrentIndex(index);
+
+			ui->tableWidgetStartingCrossover->setCellWidget(i, 0, comboBoxOption);
+			ui->tableWidgetStartingCrossover->setCellWidget(i, 1, comboBoxPlayer);
+		}
+
+		ui->tableWidgetStartingCrossover->resizeColumnsToContents();
+	}
+	else if(campaignState->scenarios.at(scenario).travelOptions.startOptions == CampaignStartOptions::START_BONUS)
+	{
+		ui->listWidgetStartingBonusOption->clear();
+		for(auto & bonus : campaignState->scenarios.at(scenario).travelOptions.bonusesToChoose)
+		{
+			QListWidgetItem * item = new QListWidgetItem(StartingBonus::getBonusListTitle(bonus, map));
+			item->setData(Qt::UserRole, QVariant::fromValue(bonus));
+			ui->listWidgetStartingBonusOption->addItem(item);
+		}
+	}
+	reloadEnableState();
+}
+
+void ScenarioProperties::reloadEnableState()
+{
+	ui->pushButtonStartingAdd->setEnabled(ui->tableWidgetStartingCrossover->rowCount() < 3 && ui->listWidgetStartingBonusOption->count() < 3);
+	ui->pushButtonStartingRemove->setEnabled(ui->tableWidgetStartingCrossover->rowCount() > 0 || ui->listWidgetStartingBonusOption->count() > 0);
+}
+
+bool ScenarioProperties::showScenarioProperties(std::shared_ptr<CampaignState> campaignState, CampaignScenarioID scenario)
+{
+	if(!campaignState || scenario == CampaignScenarioID::NONE)
+		return false;
+
+	auto * dialog = new ScenarioProperties(campaignState, scenario);
+
+	dialog->setAttribute(Qt::WA_DeleteOnClose);
+
+	return dialog->exec() == QDialog::Accepted;
+}
+
+
+void ScenarioProperties::on_buttonBox_clicked(QAbstractButton * button)
+{
+	if(button == ui->buttonBox->button(QDialogButtonBox::Ok))
+	{
+		campaignState->scenarios.at(scenario).regionText = MetaString::createFromRawString(ui->plainTextEditRightClickText->toPlainText().toStdString());
+
+		campaignState->scenarios.at(scenario).regionColor = ui->comboBoxRegionColor->currentData().toInt();
+		campaignState->scenarios.at(scenario).difficulty = ui->comboBoxDefaultDifficulty->currentData().toInt();
+
+		campaignState->scenarios.at(scenario).prolog.hasPrologEpilog = ui->checkBoxPrologueEnabled->isChecked();
+		campaignState->scenarios.at(scenario).prolog.prologVideo = VideoPath::builtin(ui->lineEditPrologueVideo->text().toStdString());
+		campaignState->scenarios.at(scenario).prolog.prologMusic = AudioPath::builtin(ui->lineEditPrologueMusic->text().toStdString());
+		campaignState->scenarios.at(scenario).prolog.prologVoice = AudioPath::builtin(ui->lineEditPrologueVoice->text().toStdString());
+		campaignState->scenarios.at(scenario).prolog.prologText = MetaString::createFromRawString(ui->plainTextEditPrologueText->toPlainText().toStdString());
+		campaignState->scenarios.at(scenario).epilog.hasPrologEpilog = ui->checkBoxEpilogueEnabled->isChecked();
+		campaignState->scenarios.at(scenario).epilog.prologVideo = VideoPath::builtin(ui->lineEditEpilogueVideo->text().toStdString());
+		campaignState->scenarios.at(scenario).epilog.prologMusic = AudioPath::builtin(ui->lineEditEpilogueMusic->text().toStdString());
+		campaignState->scenarios.at(scenario).epilog.prologVoice = AudioPath::builtin(ui->lineEditEpilogueVoice->text().toStdString());
+		campaignState->scenarios.at(scenario).epilog.prologText = MetaString::createFromRawString(ui->plainTextEditEpilogueText->toPlainText().toStdString());
+
+		campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.experience = ui->checkBoxCrossoverExperience->isChecked();
+		campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.primarySkills = ui->checkBoxCrossoverPrimarySkills->isChecked();
+		campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.secondarySkills = ui->checkBoxCrossoverSecondarySkills->isChecked();
+		campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.spells = ui->checkBoxCrossoverSpells->isChecked();
+		campaignState->scenarios.at(scenario).travelOptions.whatHeroKeeps.artifacts = ui->checkBoxCrossoverArtifacts->isChecked();
+
+		CampaignStartOptions startOption = CampaignStartOptions::NONE;
+		if(ui->radioButtonStartingOptionBonus->isChecked())
+			startOption = CampaignStartOptions::START_BONUS;
+		else if(ui->radioButtonStartingOptionHeroCrossover->isChecked())
+			startOption = CampaignStartOptions::HERO_CROSSOVER;
+		else if(ui->radioButtonStartingOptionStartingHero->isChecked())
+			startOption = CampaignStartOptions::HERO_OPTIONS;
+		campaignState->scenarios.at(scenario).travelOptions.startOptions = startOption;
+
+		campaignState->scenarios.at(scenario).travelOptions.artifactsKeptByHero.clear();
+		for(int i = 0; i < ui->listWidgetCrossoverArtifacts->count(); ++i)
+		{
+			auto * item = ui->listWidgetCrossoverArtifacts->item(i);
+			if(item->checkState() == Qt::Checked)
+				campaignState->scenarios.at(scenario).travelOptions.artifactsKeptByHero.emplace(i);
+		}
+
+		campaignState->scenarios.at(scenario).travelOptions.monstersKeptByHero.clear();
+		for(int i = 0; i < ui->listWidgetCrossoverCreatures->count(); ++i)
+		{
+			auto * item = ui->listWidgetCrossoverCreatures->item(i);
+			if(item->checkState() == Qt::Checked)
+				campaignState->scenarios.at(scenario).travelOptions.monstersKeptByHero.emplace(i);
+		}
+
+		campaignState->scenarios.at(scenario).preconditionRegions.clear();
+		for(int i = 0; i < ui->listWidgetPrerequisites->count(); ++i)
+		{
+			auto * item = ui->listWidgetPrerequisites->item(i);
+			if(item->checkState() == Qt::Checked)
+				campaignState->scenarios.at(scenario).preconditionRegions.emplace(i);
+		}
+
+		campaignState->scenarios.at(scenario).travelOptions.bonusesToChoose.clear();
+		if(ui->radioButtonStartingOptionBonus->isChecked())
+		{
+			campaignState->scenarios.at(scenario).travelOptions.playerColor = PlayerColor(ui->comboBoxStartingBonusPlayerPosition->currentData().toInt());
+			campaignState->scenarios.at(scenario).travelOptions.bonusesToChoose.clear();
+			for (int i = 0; i < ui->listWidgetStartingBonusOption->count(); ++i) {
+				QListWidgetItem *item = ui->listWidgetStartingBonusOption->item(i);
+				campaignState->scenarios.at(scenario).travelOptions.bonusesToChoose.push_back(item->data(Qt::UserRole).value<CampaignBonus>());
+			}
+		}
+		else if(ui->radioButtonStartingOptionHeroCrossover->isChecked() || ui->radioButtonStartingOptionStartingHero->isChecked())
+		{
+			for (int i = 0; i < ui->tableWidgetStartingCrossover->rowCount(); ++i)
+			{
+				CampaignBonus bonus;
+				bonus.type = ui->radioButtonStartingOptionHeroCrossover->isChecked() ? CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO : CampaignBonusType::HERO;
+				QComboBox* comboBoxOption = qobject_cast<QComboBox*>(ui->tableWidgetStartingCrossover->cellWidget(i, 0));
+				QComboBox* comboBoxPlayer = qobject_cast<QComboBox*>(ui->tableWidgetStartingCrossover->cellWidget(i, 1));
+				bonus.info1 = comboBoxPlayer->currentData().toInt();
+				bonus.info2 = comboBoxOption->currentData().toInt();
+				campaignState->scenarios.at(scenario).travelOptions.bonusesToChoose.push_back(bonus);
+			}
+		}
+
+		accept();
+	}
+	close();
+}
+
+void ScenarioProperties::on_pushButtonCreatureTypeAll_clicked()
+{
+	for(int i = 0; i < ui->listWidgetCrossoverCreatures->count(); ++i)
+		ui->listWidgetCrossoverCreatures->item(i)->setCheckState(Qt::CheckState::Checked);
+}
+
+void ScenarioProperties::on_pushButtonCreatureTypeNone_clicked()
+{
+	for(int i = 0; i < ui->listWidgetCrossoverCreatures->count(); ++i)
+		ui->listWidgetCrossoverCreatures->item(i)->setCheckState(Qt::CheckState::Unchecked);
+}
+
+void ScenarioProperties::on_pushButtonImport_clicked()
+{
+	auto filename = QFileDialog::getOpenFileName(this, tr("Open map"), "", tr("All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m)"));
+	if(filename.isEmpty())
+		return;
+
+	QFile file(filename);
+	
+	if (!file.open(QIODevice::ReadOnly)) {
+		QMessageBox::critical(nullptr, tr("Error"), tr("Could not open the file."));
+		return;
+	}
+	
+	QByteArray fileData = file.readAll();
+	std::vector<uint8_t> byteArray(fileData.begin(), fileData.end());
+	campaignState->mapPieces[scenario] = byteArray;
+
+	QFileInfo fileInfo(filename);
+	QString baseName = fileInfo.fileName();
+	campaignState->scenarios.at(scenario).mapName = baseName.toStdString();
+
+	reloadMapRelatedUi();
+}
+
+void ScenarioProperties::on_pushButtonExport_clicked()
+{
+	auto mapName = QString::fromStdString(campaignState->scenarios.at(scenario).mapName);
+	bool isVmap = mapName.toLower().endsWith(".vmap");
+	QString fileName = QFileDialog::getSaveFileName(nullptr, tr("Save map"), mapName, isVmap ? tr("VCMI maps (*.vmap);") : tr("HoMM3 maps (*.h3m);"));
+	if (fileName.isEmpty())
+		return;
+
+	QFile file(fileName);
+
+	if (!file.open(QIODevice::WriteOnly)) {
+		QMessageBox::critical(nullptr, tr("Error"), tr("Could not open the file."));
+		return;
+	}
+
+	auto byteArray = campaignState->mapPieces[scenario];
+	QByteArray fileData(reinterpret_cast<const char*>(byteArray.data()), byteArray.size());
+
+	file.write(fileData);
+}
+
+void ScenarioProperties::on_pushButtonRemove_clicked()
+{
+	campaignState->mapPieces.erase(scenario);
+	campaignState->scenarios.at(scenario).mapName = "";
+	reloadMapRelatedUi();
+}
+
+void ScenarioProperties::on_radioButtonStartingOptionBonus_toggled()
+{
+	ui->tabWidgetStartOptions->setCurrentIndex(0);
+	ui->listWidgetStartingBonusOption->clear();
+	ui->pushButtonStartingEdit->setEnabled(true);
+}
+
+void ScenarioProperties::on_radioButtonStartingOptionHeroCrossover_toggled()
+{
+	ui->tabWidgetStartOptions->setCurrentIndex(1);
+	ui->tableWidgetStartingCrossover->clearContents();
+	ui->tableWidgetStartingCrossover->setRowCount(0);
+	ui->tableWidgetStartingCrossover->setHorizontalHeaderLabels({tr("Source scenario"), tr("Player position")});
+	ui->tableWidgetStartingCrossover->resizeColumnsToContents();
+	ui->pushButtonStartingEdit->setEnabled(false);
+}
+
+void ScenarioProperties::on_radioButtonStartingOptionStartingHero_toggled()
+{
+	ui->tabWidgetStartOptions->setCurrentIndex(1);
+	ui->tableWidgetStartingCrossover->clearContents();
+	ui->tableWidgetStartingCrossover->setRowCount(0);
+	ui->tableWidgetStartingCrossover->setHorizontalHeaderLabels({tr("Hero"), tr("Player position")});
+	ui->tableWidgetStartingCrossover->resizeColumnsToContents();
+	ui->pushButtonStartingEdit->setEnabled(false);
+}
+
+void ScenarioProperties::on_pushButtonStartingAdd_clicked()
+{
+	if(ui->radioButtonStartingOptionHeroCrossover->isChecked() || ui->radioButtonStartingOptionStartingHero->isChecked())
+	{
+		int row = ui->tableWidgetStartingCrossover->rowCount();
+		ui->tableWidgetStartingCrossover->insertRow(row);
+
+		QComboBox* comboBoxOption = new QComboBox();
+		if(ui->radioButtonStartingOptionStartingHero->isChecked())
+		{
+			for(auto & hero : heroSelection)
+				comboBoxOption->addItem(QString::fromStdString(hero.second), hero.first);
+		}
+		else
+		{
+			for(int i = 0; i < scenario.getNum(); ++i)
+			{
+				auto tmpScenario = CampaignScenarioID(i);
+				auto text = getRegionChar(tmpScenario.getNum()) + " - " + QString::fromStdString(campaignState->scenarios.at(tmpScenario).mapName);
+				comboBoxOption->addItem(text, i);
+			}
+		}
+		QComboBox* comboBoxPlayer = new QComboBox();
+		for(int i = 0; i < ui->comboBoxStartingBonusPlayerPosition->count(); ++i) // copy from player dropdown
+			comboBoxPlayer->addItem(ui->comboBoxStartingBonusPlayerPosition->itemText(i), ui->comboBoxStartingBonusPlayerPosition->itemData(i));
+		
+		ui->tableWidgetStartingCrossover->setCellWidget(row, 0, comboBoxOption);
+		ui->tableWidgetStartingCrossover->setCellWidget(row, 1, comboBoxPlayer);
+
+		ui->tableWidgetStartingCrossover->resizeColumnsToContents();
+	}
+	else
+	{
+		CampaignBonus bonus;
+		bonus.type = CampaignBonusType::SPELL;
+		if(StartingBonus::showStartingBonus(PlayerColor(ui->comboBoxStartingBonusPlayerPosition->currentData().toInt()), map, bonus))
+		{
+			QListWidgetItem * item = new QListWidgetItem(StartingBonus::getBonusListTitle(bonus, map));
+			item->setData(Qt::UserRole, QVariant::fromValue(bonus));
+			ui->listWidgetStartingBonusOption->addItem(item);
+		}
+	}
+	reloadEnableState();
+}
+
+void ScenarioProperties::on_pushButtonStartingEdit_clicked()
+{
+	if(ui->radioButtonStartingOptionBonus->isChecked())
+	{
+		QList<QListWidgetItem *> selectedItems = ui->listWidgetStartingBonusOption->selectedItems();
+		QListWidgetItem * item = selectedItems.takeFirst();
+
+		CampaignBonus bonus = item->data(Qt::UserRole).value<CampaignBonus>();
+		if(StartingBonus::showStartingBonus(PlayerColor(ui->comboBoxStartingBonusPlayerPosition->currentData().toInt()), map, bonus))
+		{
+			item->setText(StartingBonus::getBonusListTitle(bonus, map));
+			item->setData(Qt::UserRole, QVariant::fromValue(bonus));
+		}
+	}
+}
+
+void ScenarioProperties::on_pushButtonStartingRemove_clicked()
+{
+	if(ui->radioButtonStartingOptionHeroCrossover->isChecked() || ui->radioButtonStartingOptionStartingHero->isChecked())
+	{
+		int rows = ui->tableWidgetStartingCrossover->rowCount() - 1;
+		ui->tableWidgetStartingCrossover->removeRow(rows);
+		ui->tableWidgetStartingCrossover->setRowCount(rows);
+
+		ui->tableWidgetStartingCrossover->resizeColumnsToContents();
+	}
+	else if(ui->radioButtonStartingOptionBonus->isChecked())
+	{
+		QList<QListWidgetItem *> selectedItems = ui->listWidgetStartingBonusOption->selectedItems();
+		for (QListWidgetItem *item : selectedItems)
+			delete ui->listWidgetStartingBonusOption->takeItem(ui->listWidgetStartingBonusOption->row(item));
+	}
+	reloadEnableState();
+}
+
+QString ScenarioProperties::getRegionChar(int no)
+{
+	return QString(static_cast<char>('A' + no));
+}

+ 65 - 0
mapeditor/campaigneditor/scenarioproperties.h

@@ -0,0 +1,65 @@
+/*
+ * scenarioproperties.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 <QDialog>
+
+#include "lib/constants/EntityIdentifiers.h"
+#include "lib/campaign/CampaignState.h"
+
+class CMap;
+class CampaignState;
+class QAbstractButton;
+
+namespace Ui {
+class ScenarioProperties;
+}
+
+Q_DECLARE_METATYPE(CampaignBonus)
+
+class ScenarioProperties : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit ScenarioProperties(std::shared_ptr<CampaignState> campaignState, CampaignScenarioID scenario);
+	~ScenarioProperties();
+
+	static bool showScenarioProperties(std::shared_ptr<CampaignState> campaignState, CampaignScenarioID scenario);
+	
+private slots:
+	void on_buttonBox_clicked(QAbstractButton * button);
+	void on_pushButtonCreatureTypeAll_clicked();
+	void on_pushButtonCreatureTypeNone_clicked();
+
+	void on_pushButtonImport_clicked();
+	void on_pushButtonExport_clicked();
+	void on_pushButtonRemove_clicked();
+
+	void on_radioButtonStartingOptionBonus_toggled();
+	void on_radioButtonStartingOptionHeroCrossover_toggled();
+	void on_radioButtonStartingOptionStartingHero_toggled();
+
+	void on_pushButtonStartingAdd_clicked();
+	void on_pushButtonStartingEdit_clicked();
+	void on_pushButtonStartingRemove_clicked();
+
+private:
+	Ui::ScenarioProperties *ui;
+
+	std::shared_ptr<CMap> map;
+	std::shared_ptr<CampaignState> campaignState;
+	CampaignScenarioID scenario;
+	
+	std::map<int, std::string> heroSelection;
+
+	void reloadMapRelatedUi();
+	void reloadEnableState();
+	QString getRegionChar(int no);
+};

+ 569 - 0
mapeditor/campaigneditor/scenarioproperties.ui

@@ -0,0 +1,569 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ScenarioProperties</class>
+ <widget class="QDialog" name="ScenarioProperties">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>499</width>
+    <height>660</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Scenario Properties</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tabGeneral">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <attribute name="title">
+       <string>General</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <item>
+        <widget class="QLabel" name="labelRegionName">
+         <property name="text">
+          <string>Region name</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditRegionName">
+         <property name="readOnly">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelRegionColor">
+         <property name="text">
+          <string>Region color</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxRegionColor"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelScenarioName">
+         <property name="text">
+          <string>Scenario name</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditScenarioName">
+         <property name="readOnly">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelMapFIle">
+         <property name="text">
+          <string>Map file</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEditMapFile">
+         <property name="readOnly">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout">
+         <item>
+          <widget class="QPushButton" name="pushButtonImport">
+           <property name="text">
+            <string>Import...</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="pushButtonExport">
+           <property name="text">
+            <string>Export...</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="pushButtonRemove">
+           <property name="text">
+            <string>Remove</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelDefaultDifficulty">
+         <property name="text">
+          <string>Default difficulty</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxDefaultDifficulty"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelPrerequisites">
+         <property name="text">
+          <string>Prerequisites</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QListWidget" name="listWidgetPrerequisites"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelRIghtClickText">
+         <property name="text">
+          <string>Region right-click text</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPlainTextEdit" name="plainTextEditRightClickText"/>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabPrologueEpilogue">
+      <attribute name="title">
+       <string>Prologue/Epilogue</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QGroupBox" name="groupBoxPrologue">
+         <property name="title">
+          <string>Prologue</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_8">
+          <item>
+           <widget class="QCheckBox" name="checkBoxPrologueEnabled">
+            <property name="text">
+             <string>Enabled</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_3">
+            <property name="topMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="QLabel" name="labelPrologueVideo">
+              <property name="text">
+               <string>Video</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLineEdit" name="lineEditPrologueVideo"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_4">
+            <property name="topMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="QLabel" name="labelPrologueMusic">
+              <property name="text">
+               <string>Music</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLineEdit" name="lineEditPrologueMusic"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_5">
+            <property name="topMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="QLabel" name="labelPrologueVoice">
+              <property name="text">
+               <string>Voice</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLineEdit" name="lineEditPrologueVoice"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QLabel" name="labelPrologueText">
+            <property name="text">
+             <string>Text</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPlainTextEdit" name="plainTextEditPrologueText"/>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="groupBoxEpilogue">
+         <property name="title">
+          <string>Epilogue</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_9">
+          <item>
+           <widget class="QCheckBox" name="checkBoxEpilogueEnabled">
+            <property name="text">
+             <string>Enabled</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_6">
+            <property name="topMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="QLabel" name="labelEpilogueVideo">
+              <property name="text">
+               <string>Video</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLineEdit" name="lineEditEpilogueVideo"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_8">
+            <property name="topMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="QLabel" name="labelEpilogueMusic">
+              <property name="text">
+               <string>Music</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLineEdit" name="lineEditEpilogueMusic"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_7">
+            <property name="topMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="QLabel" name="labelEpilogueVoice">
+              <property name="text">
+               <string>Voice</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLineEdit" name="lineEditEpilogueVoice"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QLabel" name="labelEpilogueText">
+            <property name="text">
+             <string>Text</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPlainTextEdit" name="plainTextEditEpilogueText"/>
+          </item>
+         </layout>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabCrossover">
+      <attribute name="title">
+       <string>Crossover</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_4">
+       <item>
+        <widget class="QGroupBox" name="groupBoxCrossoverHeroes">
+         <property name="title">
+          <string>Crossover heroes retain</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_6">
+          <item>
+           <widget class="QCheckBox" name="checkBoxCrossoverExperience">
+            <property name="text">
+             <string>Experience</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QCheckBox" name="checkBoxCrossoverPrimarySkills">
+            <property name="text">
+             <string>Primary skills</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QCheckBox" name="checkBoxCrossoverSecondarySkills">
+            <property name="text">
+             <string>Secondary skills</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QCheckBox" name="checkBoxCrossoverSpells">
+            <property name="text">
+             <string>Spells</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QCheckBox" name="checkBoxCrossoverArtifacts">
+            <property name="text">
+             <string>Artifacts</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_2">
+            <item>
+             <widget class="QListWidget" name="listWidgetCrossoverCreatures">
+              <property name="editTriggers">
+               <set>QAbstractItemView::NoEditTriggers</set>
+              </property>
+              <property name="horizontalScrollMode">
+               <enum>QAbstractItemView::ScrollPerItem</enum>
+              </property>
+              <property name="isWrapping" stdset="0">
+               <bool>true</bool>
+              </property>
+              <property name="layoutMode">
+               <enum>QListView::Batched</enum>
+              </property>
+              <property name="batchSize">
+               <number>30</number>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <layout class="QVBoxLayout" name="verticalLayout_7">
+              <property name="leftMargin">
+               <number>0</number>
+              </property>
+              <item>
+               <widget class="QPushButton" name="pushButtonCreatureTypeAll">
+                <property name="text">
+                 <string>All</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QPushButton" name="pushButtonCreatureTypeNone">
+                <property name="text">
+                 <string>None</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <spacer name="verticalSpacer">
+                <property name="orientation">
+                 <enum>Qt::Vertical</enum>
+                </property>
+                <property name="sizeHint" stdset="0">
+                 <size>
+                  <width>20</width>
+                  <height>40</height>
+                 </size>
+                </property>
+               </spacer>
+              </item>
+             </layout>
+            </item>
+           </layout>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="groupBoxArtifacts">
+         <property name="title">
+          <string>Crossover artifacts</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_5">
+          <item>
+           <widget class="QListWidget" name="listWidgetCrossoverArtifacts">
+            <property name="editTriggers">
+             <set>QAbstractItemView::NoEditTriggers</set>
+            </property>
+            <property name="horizontalScrollMode">
+             <enum>QAbstractItemView::ScrollPerItem</enum>
+            </property>
+            <property name="isWrapping" stdset="0">
+             <bool>true</bool>
+            </property>
+            <property name="layoutMode">
+             <enum>QListView::Batched</enum>
+            </property>
+            <property name="batchSize">
+             <number>30</number>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabStarting">
+      <attribute name="title">
+       <string>Starting</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_10">
+       <item>
+        <widget class="QLabel" name="labelStartingOption">
+         <property name="text">
+          <string>Starting options are</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QRadioButton" name="radioButtonStartingOptionBonus">
+         <property name="text">
+          <string>Starting bonus options</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QRadioButton" name="radioButtonStartingOptionHeroCrossover">
+         <property name="text">
+          <string>Hero crossover options</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QRadioButton" name="radioButtonStartingOptionStartingHero">
+         <property name="text">
+          <string>Starting hero options</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="Line" name="line">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QTabWidget" name="tabWidgetStartOptions">
+         <property name="currentIndex">
+          <number>0</number>
+         </property>
+         <widget class="QWidget" name="tabBonus">
+          <attribute name="title">
+           <string>Bonus</string>
+          </attribute>
+          <layout class="QVBoxLayout" name="verticalLayout_11">
+           <item>
+            <widget class="QLabel" name="labelStartingBonusPlayerPosition">
+             <property name="text">
+              <string>Player position</string>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <widget class="QComboBox" name="comboBoxStartingBonusPlayerPosition"/>
+           </item>
+           <item>
+            <widget class="QLabel" name="labelStartingBonusOption">
+             <property name="text">
+              <string>Starting bonus option</string>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <widget class="QListWidget" name="listWidgetStartingBonusOption"/>
+           </item>
+          </layout>
+         </widget>
+         <widget class="QWidget" name="tabCrossoverStartingHero">
+          <attribute name="title">
+           <string>Crossover/ Starting hero</string>
+          </attribute>
+          <layout class="QVBoxLayout" name="verticalLayout_12">
+           <item>
+            <widget class="QTableWidget" name="tableWidgetStartingCrossover"/>
+           </item>
+          </layout>
+         </widget>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_9">
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <widget class="QPushButton" name="pushButtonStartingAdd">
+           <property name="text">
+            <string>Add...</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="pushButtonStartingEdit">
+           <property name="text">
+            <string>Edit...</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="pushButtonStartingRemove">
+           <property name="text">
+            <string>Remove</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 370 - 0
mapeditor/campaigneditor/startingbonus.cpp

@@ -0,0 +1,370 @@
+/*
+ * startingbonus.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 "startingbonus.h"
+#include "ui_startingbonus.h"
+
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/campaign/CampaignState.h"
+#include "../../lib/entities/building/CBuilding.h"
+#include "../../lib/entities/hero/CHero.h"
+#include "../../lib/mapping/CMap.h"
+#include "../../lib/CArtHandler.h"
+#include "../../lib/CSkillHandler.h"
+#include "../../lib/CCreatureHandler.h"
+#include "../../lib/spells/CSpellHandler.h"
+
+StartingBonus::StartingBonus(PlayerColor color, std::shared_ptr<CMap> map, CampaignBonus bonus):
+	ui(new Ui::StartingBonus),
+	bonus(bonus),
+	map(map),
+	color(color)
+{
+	ui->setupUi(this);
+
+	setWindowTitle(tr("Edit Starting Bonus"));
+	
+	setWindowModality(Qt::ApplicationModal);
+
+	ui->tabWidget->tabBar()->hide();
+	ui->tabWidget->setStyleSheet("QTabWidget::pane { border: 0; }");
+
+	initControls();
+
+	loadBonus();
+
+	show();
+}
+
+StartingBonus::~StartingBonus()
+{
+	delete ui;
+}
+
+void StartingBonus::initControls()
+{
+	std::map<int, std::string> heroSelection;
+	for(auto & hero : map->heroesOnMap)
+		if(hero->getOwner() == color || color == PlayerColor::CANNOT_DETERMINE)
+			heroSelection.emplace(hero->getHeroTypeID(), hero->getNameTranslated());
+	heroSelection.emplace(0xFFFD, tr("Strongest").toStdString());
+	heroSelection.emplace(0xFFFE, tr("Generated").toStdString());
+	heroSelection.emplace(0xFFFF, tr("Random").toStdString());
+
+	for(auto & hero : heroSelection)
+	{
+		ui->comboBoxSpellRecipient->addItem(QString::fromStdString(hero.second), QVariant(hero.first));
+		ui->comboBoxCreatureRecipient->addItem(QString::fromStdString(hero.second), QVariant(hero.first));
+		ui->comboBoxArtifactRecipient->addItem(QString::fromStdString(hero.second), QVariant(hero.first));
+		ui->comboBoxSpellScrollRecipient->addItem(QString::fromStdString(hero.second), QVariant(hero.first));
+		ui->comboBoxPrimarySkillRecipient->addItem(QString::fromStdString(hero.second), QVariant(hero.first));
+		ui->comboBoxSecondarySkillRecipient->addItem(QString::fromStdString(hero.second), QVariant(hero.first));
+	}
+
+	for(auto const & objectPtr : LIBRARY->spellh->objects)
+	{
+		ui->comboBoxSpellSpell->addItem(QString::fromStdString(objectPtr->getNameTranslated()), QVariant(objectPtr->getId()));
+		ui->comboBoxSpellScrollSpell->addItem(QString::fromStdString(objectPtr->getNameTranslated()), QVariant(objectPtr->getId()));
+	}
+
+	for(auto const & objectPtr : LIBRARY->creh->objects)
+		ui->comboBoxCreatureCreatureType->addItem(QString::fromStdString(objectPtr->getNameSingularTranslated()), QVariant(objectPtr->getId()));
+	
+	if(map->players[color].hasMainTown)
+		for(auto & town : map->towns)
+			if(town->pos == map->players[color].posOfMainTown)
+			{
+				for(auto & building : town->getTown()->buildings)
+					ui->comboBoxBuildingBuilding->addItem(QString::fromStdString(building.second->getNameTranslated()), QVariant(building.first.getNum()));
+				break;
+			}
+
+	for(auto const & objectPtr : LIBRARY->arth->objects)
+		if(!vstd::contains(std::vector<ArtifactID>({
+			ArtifactID::SPELL_SCROLL,
+			ArtifactID::SPELLBOOK,
+			ArtifactID::GRAIL,
+			ArtifactID::AMMO_CART,
+			ArtifactID::BALLISTA,
+			ArtifactID::CATAPULT,
+			ArtifactID::FIRST_AID_TENT,
+		}), objectPtr->getId()))
+			ui->comboBoxArtifactArtifact->addItem(QString::fromStdString(objectPtr->getNameTranslated()), QVariant(objectPtr->getId()));
+
+	for(auto const & objectPtr : LIBRARY->skillh->objects)
+		ui->comboBoxSecondarySkillSecondarySkill->addItem(QString::fromStdString(objectPtr->getNameTranslated()), QVariant(objectPtr->getId()));
+	
+	ui->comboBoxSecondarySkillMastery->addItem(tr("Basic"), QVariant(0));
+	ui->comboBoxSecondarySkillMastery->addItem(tr("Advanced"), QVariant(1));
+	ui->comboBoxSecondarySkillMastery->addItem(tr("Expert"), QVariant(2));
+
+	ui->comboBoxResourceResourceType->addItem(tr("Wood"), QVariant(0));
+	ui->comboBoxResourceResourceType->addItem(tr("Mercury"), QVariant(1));
+	ui->comboBoxResourceResourceType->addItem(tr("Ore"), QVariant(2));
+	ui->comboBoxResourceResourceType->addItem(tr("Sulfur"), QVariant(3));
+	ui->comboBoxResourceResourceType->addItem(tr("Crystal"), QVariant(4));
+	ui->comboBoxResourceResourceType->addItem(tr("Gems"), QVariant(5));
+	ui->comboBoxResourceResourceType->addItem(tr("Gold"), QVariant(6));
+	ui->comboBoxResourceResourceType->addItem(tr("Common (Wood and Ore)"), QVariant(0xFD));
+	ui->comboBoxResourceResourceType->addItem(tr("Rare (Mercury, Sulfur, Crystal, Gems)"), QVariant(0xFE));
+}
+
+void StartingBonus::loadBonus()
+{
+	auto setComboBoxValue = [](QComboBox * comboBox, QVariant data){
+		int index = comboBox->findData(data);
+		if(index != -1)
+			comboBox->setCurrentIndex(index);
+	};
+
+	switch (bonus.type)
+	{
+	case CampaignBonusType::SPELL:
+		ui->radioButtonSpell->setChecked(true);
+		on_radioButtonSpell_toggled();
+		setComboBoxValue(ui->comboBoxSpellRecipient, bonus.info1);
+		setComboBoxValue(ui->comboBoxSpellSpell, bonus.info2);
+		break;
+	case CampaignBonusType::MONSTER:
+		ui->radioButtonCreature->setChecked(true);
+		on_radioButtonCreature_toggled();
+		setComboBoxValue(ui->comboBoxCreatureRecipient, bonus.info1);
+		setComboBoxValue(ui->comboBoxCreatureCreatureType, bonus.info2);
+		ui->spinBoxCreatureQuantity->setValue(bonus.info3);
+		break;
+	case CampaignBonusType::BUILDING:
+		ui->radioButtonBuilding->setChecked(true);
+		on_radioButtonBuilding_toggled();
+		setComboBoxValue(ui->comboBoxBuildingBuilding, bonus.info1);
+		break;
+	case CampaignBonusType::ARTIFACT:
+		ui->radioButtonArtifact->setChecked(true);
+		on_radioButtonArtifact_toggled();
+		setComboBoxValue(ui->comboBoxArtifactRecipient, bonus.info1);
+		setComboBoxValue(ui->comboBoxArtifactArtifact, bonus.info2);
+		break;
+	case CampaignBonusType::SPELL_SCROLL:
+		ui->radioButtonSpellScroll->setChecked(true);
+		on_radioButtonSpellScroll_toggled();
+		setComboBoxValue(ui->comboBoxSpellScrollRecipient, bonus.info1);
+		setComboBoxValue(ui->comboBoxSpellScrollSpell, bonus.info2);
+		break;
+	case CampaignBonusType::PRIMARY_SKILL:
+		ui->radioButtonPrimarySkill->setChecked(true);
+		on_radioButtonPrimarySkill_toggled();
+		setComboBoxValue(ui->comboBoxPrimarySkillRecipient, bonus.info1);
+		ui->spinBoxPrimarySkillAttack->setValue((bonus.info2 >> 0) & 0xff);
+		ui->spinBoxPrimarySkillDefense->setValue((bonus.info2 >> 8) & 0xff);
+		ui->spinBoxPrimarySkillSpell->setValue((bonus.info2 >> 16) & 0xff);
+		ui->spinBoxPrimarySkillKnowledge->setValue((bonus.info2 >> 24) & 0xff);
+		break;
+	case CampaignBonusType::SECONDARY_SKILL:
+		ui->radioButtonSecondarySkill->setChecked(true);
+		on_radioButtonSecondarySkill_toggled();
+		setComboBoxValue(ui->comboBoxSecondarySkillRecipient, bonus.info1);
+		setComboBoxValue(ui->comboBoxSecondarySkillSecondarySkill, bonus.info2);
+		setComboBoxValue(ui->comboBoxSecondarySkillMastery, bonus.info3);
+		break;
+	case CampaignBonusType::RESOURCE:
+		ui->radioButtonResource->setChecked(true);
+		on_radioButtonResource_toggled();
+		setComboBoxValue(ui->comboBoxResourceResourceType, bonus.info1);
+		ui->spinBoxResourceQuantity->setValue(bonus.info2);
+		break;
+	
+	default:
+		break;
+	}
+}
+
+void StartingBonus::saveBonus()
+{
+	if(ui->radioButtonSpell->isChecked())
+		bonus.type = CampaignBonusType::SPELL;
+	else if(ui->radioButtonCreature->isChecked())
+		bonus.type = CampaignBonusType::MONSTER;
+	else if(ui->radioButtonBuilding->isChecked())
+		bonus.type = CampaignBonusType::BUILDING;
+	else if(ui->radioButtonArtifact->isChecked())
+		bonus.type = CampaignBonusType::ARTIFACT;
+	else if(ui->radioButtonSpellScroll->isChecked())
+		bonus.type = CampaignBonusType::SPELL_SCROLL;
+	else if(ui->radioButtonPrimarySkill->isChecked())
+		bonus.type = CampaignBonusType::PRIMARY_SKILL;
+	else if(ui->radioButtonSecondarySkill->isChecked())
+		bonus.type = CampaignBonusType::SECONDARY_SKILL;
+	else if(ui->radioButtonResource->isChecked())
+		bonus.type = CampaignBonusType::RESOURCE;
+
+	bonus.info1 = 0;
+	bonus.info2 = 0;
+	bonus.info3 = 0;
+
+	switch (bonus.type)
+	{
+	case CampaignBonusType::SPELL:
+		bonus.info1 = ui->comboBoxSpellRecipient->currentData().toInt();
+		bonus.info2 = ui->comboBoxSpellSpell->currentData().toInt();
+		break;
+	case CampaignBonusType::MONSTER:
+		bonus.info1 = ui->comboBoxCreatureRecipient->currentData().toInt();
+		bonus.info2 = ui->comboBoxCreatureCreatureType->currentData().toInt();
+		bonus.info3 = ui->spinBoxCreatureQuantity->value();
+		break;
+	case CampaignBonusType::BUILDING:
+		bonus.info1 = ui->comboBoxBuildingBuilding->currentData().toInt();
+		break;
+	case CampaignBonusType::ARTIFACT:
+		bonus.info1 = ui->comboBoxArtifactRecipient->currentData().toInt();
+		bonus.info2 = ui->comboBoxArtifactArtifact->currentData().toInt();
+		break;
+	case CampaignBonusType::SPELL_SCROLL:
+		bonus.info1 = ui->comboBoxSpellScrollRecipient->currentData().toInt();
+		bonus.info2 = ui->comboBoxSpellScrollSpell->currentData().toInt();
+		break;
+	case CampaignBonusType::PRIMARY_SKILL:
+		bonus.info1 = ui->comboBoxPrimarySkillRecipient->currentData().toInt();
+		bonus.info2 |= ui->spinBoxPrimarySkillAttack->value() << 0;
+		bonus.info2 |= ui->spinBoxPrimarySkillDefense->value() << 8;
+		bonus.info2 |= ui->spinBoxPrimarySkillSpell->value() << 16;
+		bonus.info2 |= ui->spinBoxPrimarySkillKnowledge->value() << 24;
+		break;
+	case CampaignBonusType::SECONDARY_SKILL:
+		bonus.info1 = ui->comboBoxSecondarySkillRecipient->currentData().toInt();
+		bonus.info2 = ui->comboBoxSecondarySkillSecondarySkill->currentData().toInt();
+		bonus.info3 = ui->comboBoxSecondarySkillMastery->currentData().toInt();
+		break;
+	case CampaignBonusType::RESOURCE:
+		bonus.info1 = ui->comboBoxResourceResourceType->currentData().toInt();
+		bonus.info2 = ui->spinBoxResourceQuantity->value();
+		break;
+	
+	default:
+		break;
+	}
+}
+
+void StartingBonus::on_buttonBox_clicked(QAbstractButton * button)
+{
+	if(button == ui->buttonBox->button(QDialogButtonBox::Ok))
+	{
+		saveBonus();
+		accept();
+	}
+	close();
+}
+
+bool StartingBonus::showStartingBonus(PlayerColor color, std::shared_ptr<CMap> map, CampaignBonus & bonus)
+{
+	auto * dialog = new StartingBonus(color, map, bonus);
+
+	dialog->setAttribute(Qt::WA_DeleteOnClose);
+
+	auto result = dialog->exec();
+
+	if(result == QDialog::Accepted)
+		bonus = dialog->bonus;
+
+	return result == QDialog::Accepted;
+}
+
+QString StartingBonus::getBonusListTitle(CampaignBonus bonus, std::shared_ptr<CMap> map)
+{
+	auto getHeroName = [](int id){
+		MetaString tmp;
+		if(0xFFFD)
+			tmp.appendRawString(tr("strongest hero").toStdString());
+		else if(0xFFFE)
+			tmp.appendRawString(tr("generated hero").toStdString());
+		else if(0xFFFF)
+			tmp.appendRawString(tr("random hero").toStdString());
+		else
+			tmp.appendName(HeroTypeID(id));
+		return QString::fromStdString(tmp.toString());
+	};
+	auto getSpellName = [](int id){
+		MetaString tmp;
+		tmp.appendName(SpellID(id));
+		return QString::fromStdString(tmp.toString());
+	};
+	auto getMonsterName = [](int id, int amount){
+		MetaString tmp;
+		tmp.appendName(CreatureID(id), amount);
+		return QString::fromStdString(tmp.toString());
+	};
+	auto getArtifactName = [](int id){
+		MetaString tmp;
+		tmp.appendName(ArtifactID(id));
+		return QString::fromStdString(tmp.toString());
+	};
+
+	switch (bonus.type)
+	{
+	case CampaignBonusType::SPELL:
+		return tr("%1 spell for %2").arg(getSpellName(bonus.info2)).arg(getHeroName(bonus.info1));
+	case CampaignBonusType::MONSTER:
+		return tr("%1 %2 for %3").arg(bonus.info3).arg(getMonsterName(bonus.info2, bonus.info3)).arg(getHeroName(bonus.info1));
+	case CampaignBonusType::BUILDING:
+		return tr("Building");
+	case CampaignBonusType::ARTIFACT:
+		return tr("%1 artifact for %2").arg(getArtifactName(bonus.info2)).arg(getHeroName(bonus.info1));
+	case CampaignBonusType::SPELL_SCROLL:
+		return tr("%1 spell scroll for %2").arg(getSpellName(bonus.info2)).arg(getHeroName(bonus.info1));
+	case CampaignBonusType::PRIMARY_SKILL:
+		return tr("Primary skill (Attack: %1, Defense: %2, Spell: %3, Knowledge: %4) for %5").arg((bonus.info2 >> 0) & 0xff).arg((bonus.info2 >> 8) & 0xff).arg((bonus.info2 >> 16) & 0xff).arg((bonus.info2 >> 24) & 0xff).arg(getHeroName(bonus.info1));
+	case CampaignBonusType::SECONDARY_SKILL:
+		return tr("Secondary skill");
+	case CampaignBonusType::RESOURCE:
+		return tr("Resource");
+	}
+	return {};
+}
+
+void StartingBonus::on_radioButtonSpell_toggled()
+{
+	ui->tabWidget->setCurrentIndex(0);
+}
+
+void StartingBonus::on_radioButtonCreature_toggled()
+{
+	ui->tabWidget->setCurrentIndex(1);
+}
+
+void StartingBonus::on_radioButtonBuilding_toggled()
+{
+	ui->tabWidget->setCurrentIndex(2);
+}
+
+void StartingBonus::on_radioButtonArtifact_toggled()
+{
+	ui->tabWidget->setCurrentIndex(3);
+}
+
+void StartingBonus::on_radioButtonSpellScroll_toggled()
+{
+	ui->tabWidget->setCurrentIndex(4);
+}
+
+void StartingBonus::on_radioButtonPrimarySkill_toggled()
+{
+	ui->tabWidget->setCurrentIndex(5);
+}
+
+void StartingBonus::on_radioButtonSecondarySkill_toggled()
+{
+	ui->tabWidget->setCurrentIndex(6);
+}
+
+void StartingBonus::on_radioButtonResource_toggled()
+{
+	ui->tabWidget->setCurrentIndex(7);
+}
+

+ 55 - 0
mapeditor/campaigneditor/startingbonus.h

@@ -0,0 +1,55 @@
+/*
+ * startingbonus.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 <QDialog>
+
+#include "lib/constants/EntityIdentifiers.h"
+#include "lib/campaign/CampaignState.h"
+
+struct CampaignBonus;
+class CMap;
+
+namespace Ui {
+class StartingBonus;
+}
+
+class StartingBonus : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit StartingBonus(PlayerColor color, std::shared_ptr<CMap> map, CampaignBonus bonus);
+	~StartingBonus();
+
+	static bool showStartingBonus(PlayerColor color, std::shared_ptr<CMap> map, CampaignBonus & bonus);
+	static QString getBonusListTitle(CampaignBonus bonus, std::shared_ptr<CMap> map);
+
+private slots:
+	void on_radioButtonSpell_toggled();
+	void on_radioButtonCreature_toggled();
+	void on_radioButtonBuilding_toggled();
+	void on_radioButtonArtifact_toggled();
+	void on_radioButtonSpellScroll_toggled();
+	void on_radioButtonPrimarySkill_toggled();
+	void on_radioButtonSecondarySkill_toggled();
+	void on_radioButtonResource_toggled();
+	void on_buttonBox_clicked(QAbstractButton * button);
+
+private:
+	Ui::StartingBonus *ui;
+
+	CampaignBonus bonus;
+	std::shared_ptr<CMap> map;
+	PlayerColor color;
+
+	void initControls();
+	void loadBonus();
+	void saveBonus();
+};

+ 468 - 0
mapeditor/campaigneditor/startingbonus.ui

@@ -0,0 +1,468 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>StartingBonus</class>
+ <widget class="QDialog" name="StartingBonus">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>499</width>
+    <height>644</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Scenario Properties</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="labelSelBonusType">
+     <property name="text">
+      <string>Select a bonus type</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="radioButtonSpell">
+     <property name="text">
+      <string>Spell</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="radioButtonCreature">
+     <property name="text">
+      <string>Creature</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="radioButtonBuilding">
+     <property name="text">
+      <string>Building</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="radioButtonArtifact">
+     <property name="text">
+      <string>Artifact</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="radioButtonSpellScroll">
+     <property name="text">
+      <string>Spell scroll</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="radioButtonPrimarySkill">
+     <property name="text">
+      <string>Primary skill</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="radioButtonSecondarySkill">
+     <property name="text">
+      <string>Secondary skill</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="radioButtonResource">
+     <property name="text">
+      <string>Resource</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tabSpell">
+      <attribute name="title">
+       <string>Spell</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QLabel" name="labelSpellRecipient">
+         <property name="text">
+          <string>Recipient</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxSpellRecipient"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelSpellSpell">
+         <property name="text">
+          <string>Spell</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxSpellSpell"/>
+       </item>
+       <item>
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabCreature">
+      <attribute name="title">
+       <string>Creature</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <item>
+        <widget class="QLabel" name="labelCreatureRecipient">
+         <property name="text">
+          <string>Recipient</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxCreatureRecipient"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelCreatureCreatureType">
+         <property name="text">
+          <string>Creature type</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxCreatureCreatureType"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelCreatureQuantity">
+         <property name="text">
+          <string>Quantity</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QSpinBox" name="spinBoxCreatureQuantity"/>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabBuilding">
+      <attribute name="title">
+       <string>Building</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_4">
+       <item>
+        <widget class="QLabel" name="labelBuildingBuilding">
+         <property name="text">
+          <string>Building</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxBuildingBuilding"/>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_3">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabArtifact">
+      <attribute name="title">
+       <string>Artifact</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_5">
+       <item>
+        <widget class="QLabel" name="labelArtifactRecipient">
+         <property name="text">
+          <string>Recipient</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxArtifactRecipient"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelArtifactArtifact">
+         <property name="text">
+          <string>Artifact</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxArtifactArtifact"/>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_4">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabSpellScroll">
+      <attribute name="title">
+       <string>Spell scroll</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_6">
+       <item>
+        <widget class="QLabel" name="labelSpellScrollRecipient">
+         <property name="text">
+          <string>Recipient</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxSpellScrollRecipient"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelSpellScrollSpell">
+         <property name="text">
+          <string>Spell</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxSpellScrollSpell"/>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_5">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabPrimarySkill">
+      <attribute name="title">
+       <string>Primary skill</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_7">
+       <item>
+        <widget class="QLabel" name="labelPrimarySkillRecipient">
+         <property name="text">
+          <string>Recipient</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxPrimarySkillRecipient"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelPrimarySkillAttack">
+         <property name="text">
+          <string>Attack skill</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QSpinBox" name="spinBoxPrimarySkillAttack"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelPrimarySkillDefense">
+         <property name="text">
+          <string>Defense skill</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QSpinBox" name="spinBoxPrimarySkillDefense"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelPrimarySkillSpell">
+         <property name="text">
+          <string>Spell power</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QSpinBox" name="spinBoxPrimarySkillSpell"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelPrimarySkillKnowledge">
+         <property name="text">
+          <string>Knowledge</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QSpinBox" name="spinBoxPrimarySkillKnowledge"/>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_6">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabSecondarySkill">
+      <attribute name="title">
+       <string>Scondary skill</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_8">
+       <item>
+        <widget class="QLabel" name="labelSecondarySkillRecipient">
+         <property name="text">
+          <string>Recipient</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxSecondarySkillRecipient"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelSecondarySkillSecondarySkill">
+         <property name="text">
+          <string>Secondary skill</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxSecondarySkillSecondarySkill"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelSecondarySkillMastery">
+         <property name="text">
+          <string>Mastery</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxSecondarySkillMastery"/>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_7">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabResource">
+      <attribute name="title">
+       <string>Resource</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_9">
+       <item>
+        <widget class="QLabel" name="labelResourceResourceType">
+         <property name="text">
+          <string>Resource type</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="comboBoxResourceResourceType"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelResourceQuantity">
+         <property name="text">
+          <string>Quantity</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QSpinBox" name="spinBoxResourceQuantity"/>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_8">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 109 - 0
mapeditor/helper.cpp

@@ -0,0 +1,109 @@
+/*
+ * helper.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 "helper.h"
+#include "mapcontroller.h"
+
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/filesystem/CMemoryBuffer.h"
+#include "../lib/filesystem/CFilesystemLoader.h"
+#include "../lib/filesystem/CZipSaver.h"
+#include "../lib/campaign/CampaignHandler.h"
+#include "../lib/mapping/CMapService.h"
+#include "../lib/mapping/CMap.h"
+#include "../lib/mapping/CMap.h"
+#include "../lib/mapping/MapFormatJson.h"
+#include "../lib/modding/ModIncompatibility.h"
+
+std::unique_ptr<CMap> Helper::openMapInternal(const QString & filenameSelect)
+{
+	QFileInfo fi(filenameSelect);
+	std::string fname = fi.fileName().toStdString();
+	std::string fdir = fi.dir().path().toStdString();
+	
+	ResourcePath resId("MAPEDITOR/" + fname, EResType::MAP);
+	
+	//addFilesystem takes care about memory deallocation if case of failure, no memory leak here
+	auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0);
+	CResourceHandler::removeFilesystem("local", "mapEditor");
+	CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem);
+	
+	if(!CResourceHandler::get("mapEditor")->existsResource(resId))
+		throw std::runtime_error("Cannot open map from this folder");
+	
+	CMapService mapService;
+	if(auto header = mapService.loadMapHeader(resId))
+	{
+		auto missingMods = CMapService::verifyMapHeaderMods(*header);
+		ModIncompatibility::ModList modList;
+		for(const auto & m : missingMods)
+			modList.push_back(m.second.name);
+		
+		if(!modList.empty())
+			throw ModIncompatibility(modList);
+		
+		return mapService.loadMap(resId, nullptr);
+	}
+	else
+		throw std::runtime_error("Corrupted map");
+}
+
+std::shared_ptr<CampaignState> Helper::openCampaignInternal(const QString & filenameSelect)
+{
+	QFileInfo fi(filenameSelect);
+	std::string fname = fi.fileName().toStdString();
+	std::string fdir = fi.dir().path().toStdString();
+	
+	ResourcePath resId("MAPEDITOR/" + fname, EResType::CAMPAIGN);
+	
+	//addFilesystem takes care about memory deallocation if case of failure, no memory leak here
+	auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0);
+	CResourceHandler::removeFilesystem("local", "mapEditor");
+	CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem);
+	
+	if(!CResourceHandler::get("mapEditor")->existsResource(resId))
+		throw std::runtime_error("Cannot open campaign from this folder");
+	if(auto campaign = CampaignHandler::getCampaign(resId.getName()))
+		return campaign;
+	else
+		throw std::runtime_error("Corrupted campaign");
+}
+
+void Helper::saveCampaign(std::shared_ptr<CampaignState> campaignState, const QString & filename)
+{
+	auto jsonCampaign = CampaignHandler::writeHeaderToJson(*campaignState);
+	
+	std::shared_ptr<CIOApi> io(new CDefaultIOApi());
+	auto saver = std::make_shared<CZipSaver>(io, filename.toStdString());
+	for(auto & scenario : campaignState->allScenarios())
+	{
+		auto map = campaignState->getMap(scenario, nullptr);
+		MapController::repairMap(map.get());
+		CMemoryBuffer serializeBuffer;
+		{
+			CMapSaverJson jsonSaver(&serializeBuffer);
+			jsonSaver.saveMap(map);
+		}
+
+		auto mapName = boost::algorithm::to_lower_copy(campaignState->scenario(scenario).mapName);
+		if(!boost::ends_with(mapName, ".vmap"))
+			mapName = boost::replace_all_copy(mapName, ".h3m", std::string("")) + ".vmap";
+
+		auto stream = saver->addFile(mapName);
+		stream->write(reinterpret_cast<const ui8 *>(serializeBuffer.getBuffer().data()), serializeBuffer.getSize());
+
+		jsonCampaign["scenarios"].Vector().push_back(CampaignHandler::writeScenarioToJson(campaignState->scenario(scenario)));
+		jsonCampaign["scenarios"].Vector().back()["map"].String() = mapName;
+	}
+
+	auto jsonCampaignStr = jsonCampaign.toString();
+	saver->addFile("header.json")->write(reinterpret_cast<const ui8 *>(jsonCampaignStr.data()), jsonCampaignStr.length());
+}

+ 21 - 0
mapeditor/helper.h

@@ -0,0 +1,21 @@
+/*
+ * helper.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 CMap;
+class CampaignState;
+
+namespace Helper
+{
+	std::unique_ptr<CMap> openMapInternal(const QString &);
+	std::shared_ptr<CampaignState> openCampaignInternal(const QString &);
+	void saveCampaign(std::shared_ptr<CampaignState> campaignState, const QString & filename);
+}

+ 15 - 88
mapeditor/mainwindow.cpp

@@ -25,14 +25,11 @@
 #include "../lib/CConfigHandler.h"
 #include "../lib/CConsoleHandler.h"
 #include "../lib/filesystem/Filesystem.h"
-#include "../lib/filesystem/CMemoryBuffer.h"
 #include "../lib/GameConstants.h"
 #include "../lib/campaign/CampaignHandler.h"
 #include "../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../lib/mapObjects/ObjectTemplate.h"
-#include "../lib/mapping/CMapService.h"
-#include "../lib/mapping/CMap.h"
 #include "../lib/mapping/CMapEditManager.h"
 #include "../lib/mapping/MapFormat.h"
 #include "../lib/mapping/MapFormatJson.h"
@@ -40,7 +37,6 @@
 #include "../lib/RoadHandler.h"
 #include "../lib/RiverHandler.h"
 #include "../lib/TerrainHandler.h"
-#include "../lib/filesystem/CFilesystemLoader.h"
 
 #include "maphandler.h"
 #include "graphics.h"
@@ -51,6 +47,8 @@
 #include "mapsettings/translations.h"
 #include "playersettings.h"
 #include "validator.h"
+#include "helper.h"
+#include "campaigneditor/campaigneditor.h"
 
 QJsonValue jsonFromPixmap(const QPixmap &p)
 {
@@ -237,6 +235,7 @@ MainWindow::MainWindow(QWidget* parent) :
 	ui->actionZoom_in->setIcon(QIcon{":/icons/zoom_plus.png"});
 	ui->actionZoom_out->setIcon(QIcon{":/icons/zoom_minus.png"});
 	ui->actionZoom_reset->setIcon(QIcon{":/icons/zoom_zero.png"});
+	ui->actionCampaignEditor->setIcon(QIcon{":/icons/mapeditor.64x64.png"});
 
 	loadUserSettings(); //For example window size
 	setTitle();
@@ -363,65 +362,11 @@ void MainWindow::initializeMap(bool isNew)
 	onPlayersChanged();
 }
 
-std::unique_ptr<CMap> MainWindow::openMapInternal(const QString & filenameSelect)
-{
-	QFileInfo fi(filenameSelect);
-	std::string fname = fi.fileName().toStdString();
-	std::string fdir = fi.dir().path().toStdString();
-	
-	ResourcePath resId("MAPEDITOR/" + fname, EResType::MAP);
-	
-	//addFilesystem takes care about memory deallocation if case of failure, no memory leak here
-	auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0);
-	CResourceHandler::removeFilesystem("local", "mapEditor");
-	CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem);
-	
-	if(!CResourceHandler::get("mapEditor")->existsResource(resId))
-		throw std::runtime_error("Cannot open map from this folder");
-	
-	CMapService mapService;
-	if(auto header = mapService.loadMapHeader(resId))
-	{
-		auto missingMods = CMapService::verifyMapHeaderMods(*header);
-		ModIncompatibility::ModList modList;
-		for(const auto & m : missingMods)
-			modList.push_back(m.second.name);
-		
-		if(!modList.empty())
-			throw ModIncompatibility(modList);
-		
-		return mapService.loadMap(resId, nullptr);
-	}
-	else
-		throw std::runtime_error("Corrupted map");
-}
-
-std::shared_ptr<CampaignState> MainWindow::openCampaignInternal(const QString & filenameSelect)
-{
-	QFileInfo fi(filenameSelect);
-	std::string fname = fi.fileName().toStdString();
-	std::string fdir = fi.dir().path().toStdString();
-	
-	ResourcePath resId("MAPEDITOR/" + fname, EResType::CAMPAIGN);
-	
-	//addFilesystem takes care about memory deallocation if case of failure, no memory leak here
-	auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0);
-	CResourceHandler::removeFilesystem("local", "mapEditor");
-	CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem);
-	
-	if(!CResourceHandler::get("mapEditor")->existsResource(resId))
-		throw std::runtime_error("Cannot open campaign from this folder");
-	if(auto campaign = CampaignHandler::getCampaign(resId.getName()))
-		return campaign;
-	else
-		throw std::runtime_error("Corrupted campaign");
-}
-
 bool MainWindow::openMap(const QString & filenameSelect)
 {
 	try
 	{
-		controller.setMap(openMapInternal(filenameSelect));
+		controller.setMap(Helper::openMapInternal(filenameSelect));
 	}
 	catch(const ModIncompatibility & e)
 	{
@@ -623,6 +568,14 @@ void MainWindow::on_actionSave_as_triggered()
 	saveMap();
 }
 
+void MainWindow::on_actionCampaignEditor_triggered()
+{
+	if(!getAnswerAboutUnsavedChanges())
+		return;
+
+	hide();
+	CampaignEditor::showCampaignEditor();
+}
 
 void MainWindow::on_actionNew_triggered()
 {
@@ -1382,7 +1335,7 @@ void MainWindow::on_actionh3m_converter_triggered()
 		for(auto & m : mapFiles)
 		{
 			CMapService mapService;
-			auto map = openMapInternal(m);
+			auto map = Helper::openMapInternal(m);
 			controller.repairMap(map.get());
 			mapService.saveMap(map, (saveDirectory + '/' + QFileInfo(m).completeBaseName() + ".vmap").toStdString());
 		}
@@ -1411,35 +1364,9 @@ void MainWindow::on_actionh3c_converter_triggered()
 	QFileInfo fileInfo(campaignFileDest);
 	if(fileInfo.suffix().toLower() != "vcmp")
 		campaignFileDest += ".vcmp";
-	auto campaign = openCampaignInternal(campaignFile);
-
-	auto jsonCampaign = CampaignHandler::writeHeaderToJson(*campaign);
-	
-	std::shared_ptr<CIOApi> io(new CDefaultIOApi());
-	auto saver = std::make_shared<CZipSaver>(io, campaignFileDest.toStdString());
-	for(auto & scenario : campaign->allScenarios())
-	{
-		CMapService mapService;
-		auto map = campaign->getMap(scenario, nullptr);
-		controller.repairMap(map.get());
-		CMemoryBuffer serializeBuffer;
-		{
-			CMapSaverJson jsonSaver(&serializeBuffer);
-			jsonSaver.saveMap(map);
-		}
-
-		auto mapName = boost::algorithm::to_lower_copy(campaign->scenario(scenario).mapName);
-		mapName = boost::replace_all_copy(mapName, ".h3m", std::string("")) + ".vmap";
-
-		auto stream = saver->addFile(mapName);
-		stream->write(reinterpret_cast<const ui8 *>(serializeBuffer.getBuffer().data()), serializeBuffer.getSize());
-
-		jsonCampaign["scenarios"].Vector().push_back(CampaignHandler::writeScenarioToJson(campaign->scenario(scenario)));
-		jsonCampaign["scenarios"].Vector().back()["map"].String() = mapName;
-	}
+	auto campaign = Helper::openCampaignInternal(campaignFile);
 
-	auto jsonCampaignStr = jsonCampaign.toString();
-	saver->addFile("header.json")->write(reinterpret_cast<const ui8 *>(jsonCampaignStr.data()), jsonCampaignStr.length());
+	Helper::saveCampaign(campaign, campaignFileDest);
 }
 
 void MainWindow::on_actionLock_triggered()

+ 2 - 5
mapeditor/mainwindow.h

@@ -10,8 +10,6 @@ class ObjectBrowser;
 class ObjectBrowserProxyModel;
 
 VCMI_LIB_NAMESPACE_BEGIN
-class CMap;
-class CampaignState;
 class CConsoleHandler;
 class CBasicLogConfigurator;
 class CGObjectInstance;
@@ -42,9 +40,6 @@ class MainWindow : public QMainWindow
 #endif
 	std::unique_ptr<CBasicLogConfigurator> logConfig;
 
-	std::unique_ptr<CMap> openMapInternal(const QString &);
-	std::shared_ptr<CampaignState> openCampaignInternal(const QString &);
-
 public:
     explicit MainWindow(QWidget *parent = nullptr);
     ~MainWindow();
@@ -75,6 +70,8 @@ private slots:
 
 	void on_actionSave_as_triggered();
 
+	void on_actionCampaignEditor_triggered();
+
 	void on_actionNew_triggered();
 
 	void on_actionLevel_triggered();

+ 13 - 0
mapeditor/mainwindow.ui

@@ -70,6 +70,9 @@
     <addaction name="actionSave"/>
     <addaction name="actionSave_as"/>
     <addaction name="actionExport"/>
+    <addaction name="separator"/>
+    <addaction name="actionCampaignEditor"/>
+    <addaction name="separator"/>
     <addaction name="actionh3m_converter"/>
     <addaction name="actionh3c_converter"/>
    </widget>
@@ -144,6 +147,8 @@
    <addaction name="actionOpenRecent"/>
    <addaction name="actionSave"/>
    <addaction name="separator"/>
+   <addaction name="actionCampaignEditor"/>
+   <addaction name="separator"/>
    <addaction name="actionUndo"/>
    <addaction name="actionRedo"/>
    <addaction name="separator"/>
@@ -1065,6 +1070,14 @@
     <string>Ctrl+Shift+S</string>
    </property>
   </action>
+  <action name="actionCampaignEditor">
+   <property name="text">
+    <string>Campaign editor</string>
+   </property>
+   <property name="toolTip">
+    <string>Campaign editor</string>
+   </property>
+  </action>
   <action name="actionLevel">
    <property name="text">
     <string>View underground</string>

+ 1 - 1
mapeditor/mapcontroller.cpp

@@ -93,7 +93,7 @@ void MapController::repairMap()
 	repairMap(map());
 }
 
-void MapController::repairMap(CMap * map) const
+void MapController::repairMap(CMap * map)
 {
 	if(!map)
 		return;

+ 1 - 1
mapeditor/mapcontroller.h

@@ -31,7 +31,7 @@ public:
 	void setMap(std::unique_ptr<CMap>);
 	void initObstaclePainters(CMap * map);
 	
-	void repairMap(CMap * map) const;
+	static void repairMap(CMap * map);
 	void repairMap();
 	
 	const std::unique_ptr<CMap> & getMapUniquePtr() const; //to be used for map saving