Browse Source

Merge pull request #5423 from MichalZr6/Validator_refactors

[mapeditor] Map validation results window changes
Ivan Savenko 6 months ago
parent
commit
f2f4c03fc3

+ 1 - 0
lib/constants/EntityIdentifiers.h

@@ -9,6 +9,7 @@
  */
  */
 #pragma once
 #pragma once
 
 
+#include "Global.h"
 #include "NumericConstants.h"
 #include "NumericConstants.h"
 #include "IdentifierBase.h"
 #include "IdentifierBase.h"
 
 

+ 1 - 0
mapeditor/Animation.h

@@ -13,6 +13,7 @@
 #include "../lib/GameConstants.h"
 #include "../lib/GameConstants.h"
 #include <QRgb>
 #include <QRgb>
 #include <QImage>
 #include <QImage>
+#include <QDir>
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 class JsonNode;
 class JsonNode;

+ 2 - 0
mapeditor/CMakeLists.txt

@@ -20,6 +20,7 @@ set(editor_SRCS
 		mapsettings/eventsettings.cpp
 		mapsettings/eventsettings.cpp
 		mapsettings/rumorsettings.cpp
 		mapsettings/rumorsettings.cpp
 		mapsettings/translations.cpp
 		mapsettings/translations.cpp
+		PlayerSelectionDialog.cpp
 		playersettings.cpp
 		playersettings.cpp
 		playerparams.cpp
 		playerparams.cpp
 		scenelayer.cpp
 		scenelayer.cpp
@@ -70,6 +71,7 @@ set(editor_HEADERS
 		mapsettings/eventsettings.h
 		mapsettings/eventsettings.h
 		mapsettings/rumorsettings.h
 		mapsettings/rumorsettings.h
 		mapsettings/translations.h
 		mapsettings/translations.h
+		PlayerSelectionDialog.h
 		playersettings.h
 		playersettings.h
 		playerparams.h
 		playerparams.h
 		scenelayer.h
 		scenelayer.h

+ 104 - 0
mapeditor/PlayerSelectionDialog.cpp

@@ -0,0 +1,104 @@
+/*
+ * PlayerSelectionDialog.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 "PlayerSelectionDialog.h"
+#include "../lib/mapping/CMap.h"
+#include "mainwindow.h"
+
+#include <QRadioButton>
+#include <QButtonGroup>
+#include <QDialogButtonBox>
+#include <QAction>
+#include <QLabel>
+
+
+PlayerSelectionDialog::PlayerSelectionDialog(MainWindow * mainWindow)
+	: QDialog(mainWindow), selectedPlayer(PlayerColor::NEUTRAL)
+{
+	setupDialogComponents();
+
+	int maxPlayers = 0;
+	if(mainWindow && mainWindow->controller.map())
+		maxPlayers = mainWindow->controller.map()->players.size();
+
+	for(int i = 0; i < maxPlayers; ++i)
+	{
+		PlayerColor player(i);
+		addRadioButton(mainWindow->getActionPlayer(player), player);
+	}
+}
+
+PlayerColor PlayerSelectionDialog::getSelectedPlayer() const
+{
+	return selectedPlayer;
+}
+
+void PlayerSelectionDialog::setupDialogComponents()
+{
+	setWindowTitle(tr("Select Player"));
+	setFixedWidth(dialogWidth);
+	setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
+	font.setPointSize(10);
+	setFont(font);
+
+	buttonGroup = new QButtonGroup(this);
+	buttonGroup->setExclusive(true);
+
+	QLabel * errorLabel = new QLabel(tr("Hero cannot be created as NEUTRAL"), this);
+	font.setBold(true);
+	errorLabel->setFont(font);
+	errorLabel->setWordWrap(true);
+	mainLayout.addWidget(errorLabel);
+
+	QLabel * instructionLabel = new QLabel(tr("Switch to one of the available players:"), this);
+	font.setBold(false);
+	instructionLabel->setFont(font);
+	instructionLabel->setWordWrap(true);
+	mainLayout.addWidget(instructionLabel);
+
+	QWidget * radioContainer = new QWidget(this);
+	radioContainer->setLayout(& radioButtonsLayout);
+	mainLayout.addWidget(radioContainer);
+
+	QDialogButtonBox * box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+	connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
+	connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject);
+	mainLayout.addWidget(box);
+
+	setLayout(& mainLayout);
+}
+
+void PlayerSelectionDialog::addRadioButton(QAction * action, PlayerColor player)
+{
+	auto * radioButton = new QRadioButton(action->text(), this);
+	radioButton->setEnabled(action->isEnabled());
+	// Select first available player by default
+	if(buttonGroup->buttons().isEmpty() && radioButton->isEnabled())
+	{
+		radioButton->setChecked(true);
+		selectedPlayer = player;
+	}
+
+	radioButton->setToolTip(tr("Shortcut: %1").arg(action->shortcut().toString()));
+	buttonGroup->addButton(radioButton, player.getNum());
+	radioButtonsLayout.addWidget(radioButton);
+
+	connect(radioButton, &QRadioButton::clicked, this, [this, player]()
+		{
+			selectedPlayer = player;
+		});
+
+	addAction(action);
+	connect(action, &QAction::triggered, this, [radioButton]()
+		{
+			if(radioButton->isEnabled())
+				radioButton->click();
+		});
+}

+ 45 - 0
mapeditor/PlayerSelectionDialog.h

@@ -0,0 +1,45 @@
+/*
+ * PlayerSelectionDialog.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 <QVBoxLayout>
+#include "../lib/constants/EntityIdentifiers.h"
+
+class QRadioButton;
+class QButtonGroup;
+class MainWindow;
+
+/// Dialog shown when a hero cannot be placed as NEUTRAL.
+/// Allows the user to select a valid player via checkboxes,
+/// or using the existing keyboard shortcuts from MainWindow's player QActions.
+class PlayerSelectionDialog : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit PlayerSelectionDialog(MainWindow * mainWindow);
+	PlayerColor getSelectedPlayer() const;
+
+private:
+	const int dialogWidth = 320;
+
+	QButtonGroup * buttonGroup;
+	PlayerColor selectedPlayer;
+
+	QFont font;
+	QVBoxLayout mainLayout;
+	QVBoxLayout radioButtonsLayout;
+
+	void setupDialogComponents();
+	void addRadioButton(QAction * action, PlayerColor player);
+
+};

+ 20 - 9
mapeditor/mainwindow.cpp

@@ -45,6 +45,7 @@
 #include "inspector/inspector.h"
 #include "inspector/inspector.h"
 #include "mapsettings/mapsettings.h"
 #include "mapsettings/mapsettings.h"
 #include "mapsettings/translations.h"
 #include "mapsettings/translations.h"
+#include "mapsettings/modsettings.h"
 #include "playersettings.h"
 #include "playersettings.h"
 #include "validator.h"
 #include "validator.h"
 #include "helper.h"
 #include "helper.h"
@@ -371,6 +372,10 @@ void MainWindow::initializeMap(bool isNew)
 	initialScale = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect();
 	initialScale = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect();
 	
 	
 	//enable settings
 	//enable settings
+	mapSettings = new MapSettings(controller, this);
+	connect(&controller, &MapController::requestModsUpdate,
+		mapSettings->getModSettings(), &ModSettings::updateModWidgetBasedOnMods);
+
 	ui->actionMapSettings->setEnabled(true);
 	ui->actionMapSettings->setEnabled(true);
 	ui->actionPlayers_settings->setEnabled(true);
 	ui->actionPlayers_settings->setEnabled(true);
 	ui->actionTranslations->setEnabled(true);
 	ui->actionTranslations->setEnabled(true);
@@ -1124,9 +1129,15 @@ void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item)
 
 
 void MainWindow::on_actionMapSettings_triggered()
 void MainWindow::on_actionMapSettings_triggered()
 {
 {
-	auto settingsDialog = new MapSettings(controller, this);
-	settingsDialog->setWindowModality(Qt::WindowModal);
-	settingsDialog->setModal(true);
+	if(mapSettings->isVisible())
+	{
+		mapSettings->raise();
+		mapSettings->activateWindow();
+	}
+	else
+	{
+		mapSettings->show();
+	}
 }
 }
 
 
 
 
@@ -1155,15 +1166,15 @@ void MainWindow::switchDefaultPlayer(const PlayerColor & player)
 {
 {
 	if(controller.defaultPlayer == player)
 	if(controller.defaultPlayer == player)
 		return;
 		return;
-	
-	ui->actionNeutral->blockSignals(true);
+
+	QSignalBlocker blockerNeutral(ui->actionNeutral);
 	ui->actionNeutral->setChecked(PlayerColor::NEUTRAL == player);
 	ui->actionNeutral->setChecked(PlayerColor::NEUTRAL == player);
-	ui->actionNeutral->blockSignals(false);
+
 	for(int i = 0; i < PlayerColor::PLAYER_LIMIT.getNum(); ++i)
 	for(int i = 0; i < PlayerColor::PLAYER_LIMIT.getNum(); ++i)
 	{
 	{
-		getActionPlayer(PlayerColor(i))->blockSignals(true);
-		getActionPlayer(PlayerColor(i))->setChecked(PlayerColor(i) == player);
-		getActionPlayer(PlayerColor(i))->blockSignals(false);
+		QAction * playerEntry = getActionPlayer(PlayerColor(i));
+		QSignalBlocker blocker(playerEntry); 
+		playerEntry->setChecked(PlayerColor(i) == player);
 	}
 	}
 	controller.defaultPlayer = player;
 	controller.defaultPlayer = player;
 }
 }

+ 13 - 8
mapeditor/mainwindow.h

@@ -3,11 +3,14 @@
 #include <QMainWindow>
 #include <QMainWindow>
 #include <QGraphicsScene>
 #include <QGraphicsScene>
 #include <QStandardItemModel>
 #include <QStandardItemModel>
+#include <QTranslator>
+#include <QTableWidgetItem>
 #include "mapcontroller.h"
 #include "mapcontroller.h"
 #include "resourceExtractor/ResourceConverter.h"
 #include "resourceExtractor/ResourceConverter.h"
 
 
 class ObjectBrowser;
 class ObjectBrowser;
 class ObjectBrowserProxyModel;
 class ObjectBrowserProxyModel;
+class MapSettings;
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 class CConsoleHandler;
 class CConsoleHandler;
@@ -24,7 +27,7 @@ namespace Ui
 
 
 class MainWindow : public QMainWindow
 class MainWindow : public QMainWindow
 {
 {
-    Q_OBJECT
+	Q_OBJECT
 
 
 	const QString mainWindowSizeSetting = "MainWindow/Size";
 	const QString mainWindowSizeSetting = "MainWindow/Size";
 	const QString mainWindowPositionSetting = "MainWindow/Position";
 	const QString mainWindowPositionSetting = "MainWindow/Position";
@@ -41,8 +44,8 @@ class MainWindow : public QMainWindow
 	std::unique_ptr<CBasicLogConfigurator> logConfig;
 	std::unique_ptr<CBasicLogConfigurator> logConfig;
 
 
 public:
 public:
-    explicit MainWindow(QWidget *parent = nullptr);
-    ~MainWindow();
+	explicit MainWindow(QWidget *parent = nullptr);
+	~MainWindow();
 
 
 	void initializeMap(bool isNew);
 	void initializeMap(bool isNew);
 
 
@@ -61,6 +64,11 @@ public:
 
 
 	void loadTranslation();
 	void loadTranslation();
 
 
+	QAction * getActionPlayer(const PlayerColor &);
+
+public slots:
+	void switchDefaultPlayer(const PlayerColor &);
+
 private slots:
 private slots:
 	void on_actionOpen_triggered();
 	void on_actionOpen_triggered();
 	
 	
@@ -109,8 +117,6 @@ private slots:
 	void on_actionUpdate_appearance_triggered();
 	void on_actionUpdate_appearance_triggered();
 
 
 	void on_actionRecreate_obstacles_triggered();
 	void on_actionRecreate_obstacles_triggered();
-	
-	void switchDefaultPlayer(const PlayerColor &);
 
 
 	void on_actionCut_triggered();
 	void on_actionCut_triggered();
 
 
@@ -168,8 +174,6 @@ private:
 	void preparePreview(const QModelIndex & index);
 	void preparePreview(const QModelIndex & index);
 	void addGroupIntoCatalog(const QString & groupName, bool staticOnly);
 	void addGroupIntoCatalog(const QString & groupName, bool staticOnly);
 	void addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID);
 	void addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID);
-	
-	QAction * getActionPlayer(const PlayerColor &);
 
 
 	void changeBrushState(int idx);
 	void changeBrushState(int idx);
 	void setTitle();
 	void setTitle();
@@ -186,9 +190,10 @@ private:
 	void updateRecentMenu(const QString & filenameSelect);
 	void updateRecentMenu(const QString & filenameSelect);
 
 
 private:
 private:
-    Ui::MainWindow * ui;
+	Ui::MainWindow * ui;
 	ObjectBrowserProxyModel * objectBrowser = nullptr;
 	ObjectBrowserProxyModel * objectBrowser = nullptr;
 	QGraphicsScene * scenePreview;
 	QGraphicsScene * scenePreview;
+	MapSettings * mapSettings = nullptr;
 	
 	
 	QString filename;
 	QString filename;
 	QString lastSavingDir;
 	QString lastSavingDir;

+ 125 - 50
mapeditor/mapcontroller.cpp

@@ -29,12 +29,19 @@
 #include "../lib/spells/CSpellHandler.h"
 #include "../lib/spells/CSpellHandler.h"
 #include "../lib/CRandomGenerator.h"
 #include "../lib/CRandomGenerator.h"
 #include "../lib/serializer/CMemorySerializer.h"
 #include "../lib/serializer/CMemorySerializer.h"
+#include "mapsettings/modsettings.h"
 #include "mapview.h"
 #include "mapview.h"
 #include "scenelayer.h"
 #include "scenelayer.h"
 #include "maphandler.h"
 #include "maphandler.h"
 #include "mainwindow.h"
 #include "mainwindow.h"
 #include "inspector/inspector.h"
 #include "inspector/inspector.h"
 #include "GameLibrary.h"
 #include "GameLibrary.h"
+#include "PlayerSelectionDialog.h"
+
+MapController::MapController(QObject * parent)
+	: QObject(parent)
+{
+}
 
 
 MapController::MapController(MainWindow * m): main(m)
 MapController::MapController(MainWindow * m): main(m)
 {
 {
@@ -365,7 +372,7 @@ void MapController::pasteFromClipboard(int level)
 	{
 	{
 		auto obj = CMemorySerializer::deepCopyShared(*objUniquePtr);
 		auto obj = CMemorySerializer::deepCopyShared(*objUniquePtr);
 		QString errorMsg;
 		QString errorMsg;
-		if (!canPlaceObject(level, obj.get(), errorMsg))
+		if(!canPlaceObject(obj.get(), errorMsg))
 		{
 		{
 			errors.push_back(std::move(errorMsg));
 			errors.push_back(std::move(errorMsg));
 			continue;
 			continue;
@@ -536,33 +543,96 @@ void MapController::commitObjectCreate(int level)
 	main->mapChanged();
 	main->mapChanged();
 }
 }
 
 
-bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString & error) const
+bool MapController::canPlaceObject(const CGObjectInstance * newObj, QString & error) const
+{	
+	if(newObj->ID == Obj::GRAIL) //special case for grail
+		return canPlaceGrail(newObj, error);
+	
+	if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO))
+		return canPlaceHero(newObj, error);
+	
+	return checkRequiredMods(newObj, error);
+}
+
+bool MapController::canPlaceGrail(const CGObjectInstance * grailObj, QString & error) const
 {
 {
+	assert(grailObj->ID == Obj::GRAIL);
+
 	//find all objects of such type
 	//find all objects of such type
 	int objCounter = 0;
 	int objCounter = 0;
 	for(auto o : _map->objects)
 	for(auto o : _map->objects)
 	{
 	{
-		if(o->ID == newObj->ID && o->subID == newObj->subID)
+		if(o->ID == grailObj->ID && o->subID == grailObj->subID)
 		{
 		{
 			++objCounter;
 			++objCounter;
 		}
 		}
 	}
 	}
-	
-	if(newObj->ID == Obj::GRAIL && objCounter >= 1) //special case for grail
+
+	if(objCounter >= 1)
 	{
 	{
 		error = QObject::tr("There can only be one grail object on the map.");
 		error = QObject::tr("There can only be one grail object on the map.");
 		return false; //maplimit reached
 		return false; //maplimit reached
 	}
 	}
 	
 	
-	if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO))
+	return true;
+}
+
+bool MapController::canPlaceHero(const CGObjectInstance * heroObj, QString & error) const
+{
+	assert(heroObj->ID == Obj::HERO || heroObj->ID == Obj::RANDOM_HERO);
+
+	PlayerSelectionDialog dialog(main);
+	if(dialog.exec() == QDialog::Accepted)
 	{
 	{
-		error = QObject::tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(newObj->instanceName));
-		return false;
+		main->switchDefaultPlayer(dialog.getSelectedPlayer());
+		return true;
 	}
 	}
 	
 	
+	error = tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(heroObj->instanceName));
+	return false;
+}
+
+bool MapController::checkRequiredMods(const CGObjectInstance * obj, QString & error) const
+{
+	ModCompatibilityInfo modsInfo;
+	modAssessmentObject(obj, modsInfo);
+
+	for(auto & mod : modsInfo)
+	{
+		if(!_map->mods.count(mod.first))
+		{
+			auto reply = QMessageBox::question(main,
+				tr("Missing Required Mod"), modMissingMessage(mod.second) + tr("\n\nDo you want to do that now ?"),
+				QMessageBox::Yes | QMessageBox::No);
+
+			if(reply == QMessageBox::Yes)
+			{
+				_map->mods[mod.first] = LIBRARY->modh->getModInfo(mod.first).getVerificationInfo();
+				Q_EMIT requestModsUpdate(modsInfo, true); // signal for MapSettings
+			}
+			else
+			{
+				error = tr("This object's mod is mandatory for map to remain valid.");
+				return false;
+			}
+		}
+	}
 	return true;
 	return true;
 }
 }
 
 
+QString MapController::modMissingMessage(const ModVerificationInfo & info)
+{
+	QString modName = QString::fromStdString(info.name);
+	QString submod;
+	if(!info.parent.empty())
+		submod = QObject::tr(" (submod of %1)").arg(QString::fromStdString(info.parent));
+
+	return QObject::tr("The mod '%1'%2, is required by an object on the map.\n"
+		"Add it to the map's required mods in Map->General settings.",
+		"should be consistent with Map->General menu entry translation")
+		.arg(modName, submod);
+}
+
 void MapController::undo()
 void MapController::undo()
 {
 {
 	_map->getEditManager()->getUndoManager().undo();
 	_map->getEditManager()->getUndoManager().undo();
@@ -587,70 +657,75 @@ ModCompatibilityInfo MapController::modAssessmentAll()
 		for(auto secondaryID : LIBRARY->objtypeh->knownSubObjects(primaryID))
 		for(auto secondaryID : LIBRARY->objtypeh->knownSubObjects(primaryID))
 		{
 		{
 			auto handler = LIBRARY->objtypeh->getHandlerFor(primaryID, secondaryID);
 			auto handler = LIBRARY->objtypeh->getHandlerFor(primaryID, secondaryID);
-			auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString();
-			if(modName != "core")
-				result[modName] = LIBRARY->modh->getModInfo(modName).getVerificationInfo();
+			auto modScope = handler->getModScope();
+			if(modScope != "core")
+				result[modScope] = LIBRARY->modh->getModInfo(modScope).getVerificationInfo();
 		}
 		}
 	}
 	}
 	return result;
 	return result;
 }
 }
 
 
-ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map)
+void MapController::modAssessmentObject(const CGObjectInstance * obj, ModCompatibilityInfo & result)
 {
 {
-	ModCompatibilityInfo result;
-
-	auto extractEntityMod = [&result](const auto & entity) 
+	auto extractEntityMod = [&result](const auto & entity)
 	{
 	{
 		auto modScope = entity->getModScope();
 		auto modScope = entity->getModScope();
 		if(modScope != "core")
 		if(modScope != "core")
 			result[modScope] = LIBRARY->modh->getModInfo(modScope).getVerificationInfo();
 			result[modScope] = LIBRARY->modh->getModInfo(modScope).getVerificationInfo();
 	};
 	};
 
 
-	for(auto obj : map.objects)
-	{
-		auto handler = obj->getObjectHandler();
-		auto modScope = handler->getModScope();
-		if(modScope != "core")
-			result[modScope] = LIBRARY->modh->getModInfo(modScope).getVerificationInfo();
+	auto handler = obj->getObjectHandler();
+	auto modScope = handler->getModScope();
+	if(modScope != "core")
+		result[modScope] = LIBRARY->modh->getModInfo(modScope).getVerificationInfo();
 
 
-		if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN)
+	if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN)
+	{
+		auto town = dynamic_cast<const CGTownInstance *>(obj);
+		for(const auto & spellID : town->possibleSpells)
 		{
 		{
-			auto town = dynamic_cast<CGTownInstance *>(obj.get());
-			for(const auto & spellID : town->possibleSpells)
-			{
-				if(spellID == SpellID::PRESET)
-					continue;
-				extractEntityMod(spellID.toEntity(LIBRARY));
-			}
+			if(spellID == SpellID::PRESET)
+				continue;
+			extractEntityMod(spellID.toEntity(LIBRARY));
+		}
 
 
-			for(const auto & spellID : town->obligatorySpells)
-			{
-				extractEntityMod(spellID.toEntity(LIBRARY));
-			}
+		for(const auto & spellID : town->obligatorySpells)
+		{
+			extractEntityMod(spellID.toEntity(LIBRARY));
 		}
 		}
+	}
 
 
-		if(obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO)
+	if(obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO)
+	{
+		auto hero = dynamic_cast<const CGHeroInstance *>(obj);
+		for(const auto & spellID : hero->getSpellsInSpellbook())
 		{
 		{
-			auto hero = dynamic_cast<CGHeroInstance *>(obj.get());
-			for(const auto & spellID : hero->getSpellsInSpellbook())
-			{
-				if(spellID == SpellID::PRESET || spellID == SpellID::SPELLBOOK_PRESET)
-					continue;
-				extractEntityMod(spellID.toEntity(LIBRARY));
-			}
+			if(spellID == SpellID::PRESET || spellID == SpellID::SPELLBOOK_PRESET)
+				continue;
+			extractEntityMod(spellID.toEntity(LIBRARY));
+		}
 
 
-			for(const auto & [_, slotInfo] : hero->artifactsWorn)
-			{
-				extractEntityMod(slotInfo.getArt()->getTypeId().toEntity(LIBRARY));
-			}
+		for(const auto & [_, slotInfo] : hero->artifactsWorn)
+		{
+			extractEntityMod(slotInfo.getArt()->getTypeId().toEntity(LIBRARY));
+		}
 
 
-			for(const auto & art : hero->artifactsInBackpack)
-			{
-				extractEntityMod(art.getArt()->getTypeId().toEntity(LIBRARY));
-			}
+		for(const auto & art : hero->artifactsInBackpack)
+		{
+			extractEntityMod(art.getArt()->getTypeId().toEntity(LIBRARY));
 		}
 		}
 	}
 	}
 
 
-	//TODO: terrains?
+//TODO: terrains?
+}
+
+ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map)
+{
+	ModCompatibilityInfo result;
+
+	for(auto obj : map.objects)
+	{
+		modAssessmentObject(obj.get(), result);
+	}
 	return result;
 	return result;
 }
 }

+ 24 - 3
mapeditor/mapcontroller.h

@@ -12,17 +12,20 @@
 
 
 #include "maphandler.h"
 #include "maphandler.h"
 #include "mapview.h"
 #include "mapview.h"
+#include "lib/modding/ModVerificationInfo.h"
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
-struct ModVerificationInfo;
 using ModCompatibilityInfo = std::map<std::string, ModVerificationInfo>;
 using ModCompatibilityInfo = std::map<std::string, ModVerificationInfo>;
 class EditorObstaclePlacer;
 class EditorObstaclePlacer;
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END
 
 
 class MainWindow;
 class MainWindow;
-class MapController
+class MapController : public QObject
 {
 {
+	Q_OBJECT
+
 public:
 public:
+	explicit MapController(QObject * parent = nullptr);
 	MapController(MainWindow *);
 	MapController(MainWindow *);
 	MapController(const MapController &) = delete;
 	MapController(const MapController &) = delete;
 	MapController(const MapController &&) = delete;
 	MapController(const MapController &&) = delete;
@@ -60,16 +63,34 @@ public:
 	
 	
 	bool discardObject(int level) const;
 	bool discardObject(int level) const;
 	void createObject(int level, std::shared_ptr<CGObjectInstance> obj) const;
 	void createObject(int level, std::shared_ptr<CGObjectInstance> obj) const;
-	bool canPlaceObject(int level, CGObjectInstance * obj, QString & error) const;
+	bool canPlaceObject(const CGObjectInstance * obj, QString & error) const;
+	bool canPlaceGrail(const CGObjectInstance * grailObj, QString & error) const;
+	bool canPlaceHero(const CGObjectInstance * heroObj, QString & error) const;
 	
 	
+	/// Ensures that the object's mod is listed in the map's required mods.
+	/// If the mod is missing, prompts the user to add it. Returns false if the user declines,
+	/// making the object invalid for placement.
+	bool checkRequiredMods(const CGObjectInstance * obj, QString & error) const;
+
+	/// These functions collect mod verification data for gameplay objects by scanning map objects
+	/// and their nested elements (like spells and artifacts). The gathered information
+	/// is used to assess compatibility and integrity of mods used in a given map or game state
+	static void modAssessmentObject(const CGObjectInstance * obj, ModCompatibilityInfo & result);
 	static ModCompatibilityInfo modAssessmentAll();
 	static ModCompatibilityInfo modAssessmentAll();
 	static ModCompatibilityInfo modAssessmentMap(const CMap & map);
 	static ModCompatibilityInfo modAssessmentMap(const CMap & map);
 
 
+	/// Returns formatted message string describing a missing mod requirement for the map.
+	/// Used in both warnings and confirmations related to required mod dependencies.
+	static QString modMissingMessage(const ModVerificationInfo & info);
+
 	void undo();
 	void undo();
 	void redo();
 	void redo();
 	
 	
 	PlayerColor defaultPlayer;
 	PlayerColor defaultPlayer;
 	QDialog * settingsDialog = nullptr;
 	QDialog * settingsDialog = nullptr;
+
+signals:
+	void requestModsUpdate(const ModCompatibilityInfo & mods, bool leaveCheckedUnchanged) const;
 	
 	
 private:
 private:
 	std::unique_ptr<CMap> _map;
 	std::unique_ptr<CMap> _map;

+ 7 - 2
mapeditor/mapsettings/mapsettings.cpp

@@ -25,12 +25,12 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) :
 	controller(ctrl)
 	controller(ctrl)
 {
 {
 	ui->setupUi(this);
 	ui->setupUi(this);
-	
+
+	setWindowModality(Qt::WindowModal);
 	setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
 	setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
 	
 	
 	assert(controller.map());
 	assert(controller.map());
 	controller.settingsDialog = this;
 	controller.settingsDialog = this;
-	show();
 
 
 	for(auto const & objectPtr : LIBRARY->skillh->objects)
 	for(auto const & objectPtr : LIBRARY->skillh->objects)
 	{
 	{
@@ -79,6 +79,11 @@ MapSettings::~MapSettings()
 	delete ui;
 	delete ui;
 }
 }
 
 
+ModSettings * MapSettings::getModSettings()
+{
+	return ui->mods;
+}
+
 void MapSettings::on_pushButton_clicked()
 void MapSettings::on_pushButton_clicked()
 {	
 {	
 	auto updateMapArray = [](const QListWidget * widget, auto & arr)
 	auto updateMapArray = [](const QListWidget * widget, auto & arr)

+ 4 - 0
mapeditor/mapsettings/mapsettings.h

@@ -18,6 +18,8 @@ namespace Ui {
 class MapSettings;
 class MapSettings;
 }
 }
 
 
+class ModSettings;
+
 class MapSettings : public QDialog
 class MapSettings : public QDialog
 {
 {
 	Q_OBJECT
 	Q_OBJECT
@@ -26,6 +28,8 @@ public:
 	explicit MapSettings(MapController & controller, QWidget *parent = nullptr);
 	explicit MapSettings(MapController & controller, QWidget *parent = nullptr);
 	~MapSettings();
 	~MapSettings();
 
 
+	ModSettings * getModSettings();
+
 private slots:
 private slots:
 	void on_pushButton_clicked();
 	void on_pushButton_clicked();
 
 

+ 14 - 2
mapeditor/mapsettings/modsettings.cpp

@@ -44,6 +44,8 @@ void ModSettings::initialize(MapController & c)
 	QMap<QString, QTreeWidgetItem*> addedMods;
 	QMap<QString, QTreeWidgetItem*> addedMods;
 	QSet<QString> modsToProcess;
 	QSet<QString> modsToProcess;
 	ui->treeMods->blockSignals(true);
 	ui->treeMods->blockSignals(true);
+	ui->treeMods->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+	ui->treeMods->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
 
 
 	auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const ModDescription & modInfo)
 	auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const ModDescription & modInfo)
 	{
 	{
@@ -59,6 +61,8 @@ void ModSettings::initialize(MapController & c)
 
 
 	for(const auto & modName : LIBRARY->modh->getActiveMods())
 	for(const auto & modName : LIBRARY->modh->getActiveMods())
 	{
 	{
+		if(modName == "core")
+			continue;
 		QString qmodName = QString::fromStdString(modName);
 		QString qmodName = QString::fromStdString(modName);
 		if(qmodName.split(".").size() == 1)
 		if(qmodName.split(".").size() == 1)
 		{
 		{
@@ -115,13 +119,21 @@ void ModSettings::update()
 	}
 	}
 }
 }
 
 
-void ModSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods)
+void ModSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods, bool leaveCheckedUnchanged)
 {
 {
 	//Mod management
 	//Mod management
 	auto widgetAction = [&](QTreeWidgetItem * item)
 	auto widgetAction = [&](QTreeWidgetItem * item)
 	{
 	{
 		auto modName = item->data(0, Qt::UserRole).toString().toStdString();
 		auto modName = item->data(0, Qt::UserRole).toString().toStdString();
-		item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked);
+		if(leaveCheckedUnchanged)
+		{
+			if(mods.count(modName))
+				item->setCheckState(0, Qt::Checked);
+		}
+		else
+		{
+			item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked);
+		}
 	};
 	};
 
 
 	for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i)
 	for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i)

+ 2 - 3
mapeditor/mapsettings/modsettings.h

@@ -26,6 +26,8 @@ public:
 	void initialize(MapController & map) override;
 	void initialize(MapController & map) override;
 	void update() override;
 	void update() override;
 
 
+	void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods, bool leaveCheckedUnchanged = false);
+
 private slots:
 private slots:
 	void on_modResolution_map_clicked();
 	void on_modResolution_map_clicked();
 
 
@@ -33,9 +35,6 @@ private slots:
 
 
 	void on_treeMods_itemChanged(QTreeWidgetItem *item, int column);
 	void on_treeMods_itemChanged(QTreeWidgetItem *item, int column);
 
 
-private:
-	void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods);
-
 private:
 private:
 	Ui::ModSettings *ui;
 	Ui::ModSettings *ui;
 };
 };

+ 1 - 1
mapeditor/mapview.cpp

@@ -614,7 +614,7 @@ void MapView::dropEvent(QDropEvent * event)
 	if(sc->selectionObjectsView.newObject)
 	if(sc->selectionObjectsView.newObject)
 	{
 	{
 		QString errorMsg;
 		QString errorMsg;
-		if(controller->canPlaceObject(sc->level, sc->selectionObjectsView.newObject.get(), errorMsg))
+		if(controller->canPlaceObject(sc->selectionObjectsView.newObject.get(), errorMsg))
 		{
 		{
 			auto obj = sc->selectionObjectsView.newObject;
 			auto obj = sc->selectionObjectsView.newObject;
 			controller->commitObjectCreate(sc->level);
 			controller->commitObjectCreate(sc->level);

+ 1 - 0
mapeditor/mapview.h

@@ -12,6 +12,7 @@
 
 
 #include <QGraphicsScene>
 #include <QGraphicsScene>
 #include <QGraphicsView>
 #include <QGraphicsView>
+#include <QRubberBand>
 #include "scenelayer.h"
 #include "scenelayer.h"
 #include "../lib/int3.h"
 #include "../lib/int3.h"
 
 

+ 127 - 15
mapeditor/validator.cpp

@@ -24,20 +24,12 @@ Validator::Validator(const CMap * map, QWidget *parent) :
 	ui(new Ui::Validator)
 	ui(new Ui::Validator)
 {
 {
 	ui->setupUi(this);
 	ui->setupUi(this);
+
+	screenGeometry = QApplication::primaryScreen()->availableGeometry();
 	
 	
 	setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
 	setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
 	
 	
-	show();
-	
-	setAttribute(Qt::WA_DeleteOnClose);
-
-	std::array<QString, 2> icons{":/icons/mod-update.png", ":/icons/mod-delete.png"};
-
-	for(auto & issue : Validator::validate(map))
-	{
-		auto * item = new QListWidgetItem(QIcon(icons[issue.critical ? 1 : 0]), issue.message);
-		ui->listWidget->addItem(item);
-	}
+	showValidationResults(map);
 }
 }
 
 
 Validator::~Validator()
 Validator::~Validator()
@@ -175,13 +167,11 @@ std::set<Validator::Issue> Validator::validate(const CMap * map)
 		if(map->description.empty())
 		if(map->description.empty())
 			issues.insert({ tr("Map description is not specified"), false });
 			issues.insert({ tr("Map description is not specified"), false });
 		
 		
-		//verificationfor mods
+		//verification for mods
 		for(auto & mod : MapController::modAssessmentMap(*map))
 		for(auto & mod : MapController::modAssessmentMap(*map))
 		{
 		{
 			if(!map->mods.count(mod.first))
 			if(!map->mods.count(mod.first))
-			{
-				issues.insert({ tr("Map contains object from mod \"%1\", but doesn't require it").arg(QString::fromStdString(LIBRARY->modh->getModInfo(mod.first).getVerificationInfo().name)), true });
-			}
+				issues.insert({ MapController::modMissingMessage(mod.second), true });
 		}
 		}
 	}
 	}
 	catch(const std::exception & e)
 	catch(const std::exception & e)
@@ -195,3 +185,125 @@ std::set<Validator::Issue> Validator::validate(const CMap * map)
 	
 	
 	return issues;
 	return issues;
 }
 }
+
+void Validator::showValidationResults(const CMap * map)
+{
+	show();
+	setAttribute(Qt::WA_DeleteOnClose);
+	ui->listWidget->setItemDelegate(new ValidatorItemDelegate(ui->listWidget));
+
+	for(auto const & issue : Validator::validate(map))
+	{
+		auto * item = new QListWidgetItem(QIcon(issue.critical ? ":/icons/mod-delete.png" : ":/icons/mod-update.png"),
+			issue.message, ui->listWidget);
+
+		ui->listWidget->addItem(item);
+	}
+
+	if(ui->listWidget->count() == 0)
+	{
+		QPixmap greenTick(":/icons/mod-enabled.png");
+		QString validMessage = tr("The map is valid and has no issues.");
+		auto * item = new QListWidgetItem(QIcon(greenTick), validMessage, ui->listWidget);
+		ui->listWidget->addItem(item);
+	}
+
+	ui->listWidget->updateGeometry();
+
+	adjustWindowSize();
+}
+
+void Validator::adjustWindowSize()
+{
+	const int minWidth = 350;
+	const int minHeight = 50;
+	const int padding = 30;		// reserved space for eventual scrollbars
+	const int screenMarginVertical = 300;
+	const int screenMarginHorizontal = 350;
+
+	int contentHeight = minHeight;
+	int contentWidth = minWidth;
+
+	QStyleOptionViewItem option;
+	option.initFrom(ui->listWidget);
+
+	int listWidgetWidth = ui->listWidget->viewport()->width();
+
+	for(int i = 0; i < ui->listWidget->count(); ++i)
+	{
+		option.rect = QRect(0, 0, listWidgetWidth, 0);
+		auto itemSize = ui->listWidget->itemDelegate()->sizeHint(option, ui->listWidget->model()->index(i, 0));
+		contentHeight += itemSize.height();
+		contentWidth = qMax(contentWidth, itemSize.width());
+	}
+
+	int screenWidth = screenGeometry.width();
+	int screenHeight = screenGeometry.height();
+
+	int finalWidth = qMin(contentWidth + padding, screenWidth - screenMarginHorizontal);
+	int finalHeight = qMin(contentHeight + padding, screenHeight - screenMarginVertical);
+
+	QWidget * parentWidget = ui->listWidget->parentWidget();
+	if(parentWidget)
+	{
+		parentWidget->setMinimumWidth(finalWidth + padding);
+		parentWidget->setMinimumHeight(finalHeight + padding);
+	}
+
+	ui->listWidget->resize(finalWidth, finalHeight);
+
+	move((screenWidth - finalWidth) / 2, (screenHeight - finalHeight) / 2);
+}
+
+void ValidatorItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
+{
+	painter->save();
+
+	QStyleOptionViewItem opt(option);
+	QFontMetrics metrics(option.fontMetrics);
+	initStyleOption(&opt, index);
+
+	const QRect iconRect = option.rect.adjusted(iconPadding, iconPadding, 0, 0);
+
+	const QRect textRect = option.rect.adjusted(offsetForIcon, 0, -textPaddingRight, 0);
+
+	if(!opt.icon.isNull())
+	{
+		opt.icon.paint(painter, iconRect, Qt::AlignTop | Qt::AlignLeft);
+	}
+
+	QTextOption textOption;
+
+	int textWidth = metrics.horizontalAdvance(opt.text);
+	if(textWidth + offsetForIcon + textPaddingRight > screenGeometry.width() - screenMargin)
+	{
+		textOption.setWrapMode(QTextOption::WordWrap);
+	}
+
+	painter->drawText(textRect, opt.text, textOption);
+
+	painter->restore();
+}
+
+QSize ValidatorItemDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const
+{
+	QFontMetrics metrics(option.fontMetrics);
+	QString text = index.data(Qt::DisplayRole).toString();
+	QStringList lines = text.split('\n');
+	int textWidth = minItemWidth;
+	int requiredHeight = 0;
+	for(auto line : lines)
+		textWidth = std::max(metrics.horizontalAdvance(line), textWidth);
+
+	requiredHeight = qMax(requiredHeight, lines.size() * metrics.height());
+
+	int finalWidth = qMax(textWidth + offsetForIcon, minItemWidth);
+	finalWidth = qMin(finalWidth, screenGeometry.width() - screenMargin - offsetForIcon);
+
+	QRect textBoundingRect = metrics.boundingRect(QRect(0, 0, finalWidth, 0),
+		Qt::TextWordWrap, text);
+
+	int finalHeight = qMax(textBoundingRect.height() + itemPaddingBottom, requiredHeight);
+
+	return QSize(finalWidth, finalHeight);
+}

+ 29 - 0
mapeditor/validator.h

@@ -46,4 +46,33 @@ public:
 
 
 private:
 private:
 	Ui::Validator *ui;
 	Ui::Validator *ui;
+
+	QRect screenGeometry;
+
+	void showValidationResults(const CMap * map);
+	void adjustWindowSize();
+};
+
+class ValidatorItemDelegate : public QStyledItemDelegate
+{
+public:
+	explicit ValidatorItemDelegate(QObject * parent = nullptr) : QStyledItemDelegate(parent) 
+	{
+		screenGeometry = QApplication::primaryScreen()->availableGeometry();
+	}
+
+	int itemPaddingBottom = 14;
+	int iconPadding = 4;
+	int textOffsetForIcon = 30;
+	int textPaddingRight = 10;
+	int minItemWidth = 350;
+	int screenMargin = 350;     // some reserved space from screenWidth; used if text.width > screenWidth - screenMargin 
+	
+	const int offsetForIcon = iconPadding + textOffsetForIcon;
+
+	void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+	QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+
+private:
+	QRect screenGeometry;
 };
 };

+ 1 - 1
mapeditor/windownewmap.cpp

@@ -311,7 +311,7 @@ void WindowNewMap::on_okButton_clicked()
 		nmap = f.get();
 		nmap = f.get();
 	}
 	}
 	
 	
-	nmap->mods = MapController::modAssessmentAll();
+	nmap->mods = MapController::modAssessmentMap(*nmap);
 	static_cast<MainWindow*>(parent())->controller.setMap(std::move(nmap));
 	static_cast<MainWindow*>(parent())->controller.setMap(std::move(nmap));
 	static_cast<MainWindow*>(parent())->initializeMap(true);
 	static_cast<MainWindow*>(parent())->initializeMap(true);
 	close();
 	close();