2022-10-13 01:51:55 +04:00
/*
* validator.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
*
*/
2022-09-18 03:23:17 +04:00
#include "StdInc.h"
#include "validator.h"
2023-04-17 03:01:29 +04:00
#include "mapcontroller.h"
2022-09-18 03:23:17 +04:00
#include "ui_validator.h"
2024-10-11 16:30:16 +00:00
#include "../lib/entities/hero/CHero.h"
2023-05-24 02:05:59 +03:00
#include "../lib/mapping/CMap.h"
2022-09-18 03:23:17 +04:00
#include "../lib/mapObjects/MapObjects.h"
2023-07-30 20:12:25 +03:00
#include "../lib/modding/CModHandler.h"
2024-11-09 20:29:07 +00:00
#include "../lib/modding/ModDescription.h"
2023-09-04 03:33:01 +04:00
#include "../lib/spells/CSpellHandler.h"
2022-09-18 03:23:17 +04:00
Validator :: Validator ( const CMap * map , QWidget * parent ) :
QDialog ( parent ),
ui ( new Ui :: Validator )
{
ui -> setupUi ( this );
2025-02-09 19:19:36 +01:00
screenGeometry = QApplication :: primaryScreen () -> availableGeometry ();
2025-01-06 13:39:14 +01:00
setWindowFlags ( Qt :: Dialog | Qt :: WindowTitleHint | Qt :: WindowCloseButtonHint );
2025-02-09 19:19:36 +01:00
showValidationResults ( map );
2022-09-18 03:23:17 +04:00
}
Validator ::~ Validator ()
{
delete ui ;
}
2024-08-20 00:53:48 +02:00
std :: set < Validator :: Issue > Validator :: validate ( const CMap * map )
2022-09-18 03:23:17 +04:00
{
2024-08-20 00:53:48 +02:00
std :: set < Validator :: Issue > issues ;
2022-09-18 03:23:17 +04:00
if ( ! map )
{
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Map is not loaded" ), true });
2022-09-18 03:23:17 +04:00
return issues ;
}
try
{
//check player settings
int hplayers = 0 ;
int cplayers = 0 ;
2024-08-19 00:21:11 +02:00
std :: map < PlayerColor , int > amountOfTowns ;
std :: map < PlayerColor , int > amountOfHeroes ;
2022-09-18 03:23:17 +04:00
for ( int i = 0 ; i < map -> players . size (); ++ i )
{
auto & p = map -> players [ i ];
2024-08-20 00:53:48 +02:00
if ( p . canAnyonePlay ())
amountOfTowns [ PlayerColor ( i )] = 0 ;
2022-09-18 03:23:17 +04:00
if ( p . canComputerPlay )
++ cplayers ;
if ( p . canHumanPlay )
++ hplayers ;
if ( p . allowedFactions . empty ())
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "No factions allowed for player %1" ). arg ( i ), true });
2022-09-18 03:23:17 +04:00
}
if ( hplayers + cplayers == 0 )
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "No players allowed to play this map" ), true });
2022-09-18 03:23:17 +04:00
if ( hplayers + cplayers == 1 )
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Map is allowed for one player and cannot be started" ), true });
2022-09-18 03:23:17 +04:00
if ( ! hplayers )
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "No human players allowed to play this map" ), true });
2022-09-18 03:23:17 +04:00
2024-08-20 00:53:48 +02:00
std :: set < const CHero * > allHeroesOnMap ; //used to find hero duplicated
2023-04-22 20:08:52 +04:00
2022-09-18 03:23:17 +04:00
//checking all objects in the map
for ( auto o : map -> objects )
{
2025-09-18 10:57:56 +02:00
if ( ! o )
continue ;
2022-09-18 03:23:17 +04:00
//owners for objects
if ( o -> getOwner () == PlayerColor :: UNFLAGGABLE )
{
2025-03-13 19:42:57 +00:00
if ( o -> asOwnable ())
2022-09-18 03:23:17 +04:00
{
2025-03-13 19:42:57 +00:00
issues . insert ({ tr ( "Ownable object %1 is UNFLAGGABLE but must have NEUTRAL or player owner" ). arg ( o -> instanceName . c_str ()), true });
2022-09-18 03:23:17 +04:00
}
}
2022-09-20 02:38:10 +04:00
if ( o -> getOwner () != PlayerColor :: NEUTRAL && o -> getOwner (). getNum () < map -> players . size ())
{
if ( ! map -> players [ o -> getOwner (). getNum ()]. canAnyonePlay ())
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Object %1 is assigned to non-playable player %2" ). arg ( o -> instanceName . c_str (), o -> getOwner (). toString (). c_str ()), true });
2022-09-20 02:38:10 +04:00
}
2024-08-19 00:21:11 +02:00
//count towns
2024-08-20 00:53:48 +02:00
if ( auto * ins = dynamic_cast < CGTownInstance *> ( o . get ()))
2022-09-18 03:23:17 +04:00
{
2024-08-19 00:21:11 +02:00
++ amountOfTowns [ ins -> getOwner ()];
2022-09-18 03:23:17 +04:00
}
2024-08-19 00:21:11 +02:00
//checking and counting heroes and prisons
2024-08-20 00:53:48 +02:00
if ( auto * ins = dynamic_cast < CGHeroInstance *> ( o . get ()))
2022-09-18 03:23:17 +04:00
{
if ( ins -> ID == Obj :: PRISON )
{
if ( ins -> getOwner () != PlayerColor :: NEUTRAL )
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Prison %1 must be a NEUTRAL" ). arg ( ins -> instanceName . c_str ()), true });
2022-09-18 03:23:17 +04:00
}
else
{
2024-08-19 00:21:11 +02:00
if ( ins -> getOwner () == PlayerColor :: NEUTRAL )
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Hero %1 must have an owner" ). arg ( ins -> instanceName . c_str ()), true });
2024-08-19 00:21:11 +02:00
++ amountOfHeroes [ ins -> getOwner ()];
2022-09-18 03:23:17 +04:00
}
2024-10-05 19:37:52 +00:00
if ( ins -> getHeroTypeID (). hasValue ())
2022-09-18 03:23:17 +04:00
{
2024-10-05 19:37:52 +00:00
if ( map -> allowedHeroes . count ( ins -> getHeroTypeID ()) == 0 )
issues . insert ({ tr ( "Hero %1 is prohibited by map settings" ). arg ( ins -> getHeroType () -> getNameTranslated (). c_str ()), false });
2023-04-22 20:08:52 +04:00
2024-10-05 19:37:52 +00:00
if ( ! allHeroesOnMap . insert ( ins -> getHeroType ()). second )
issues . insert ({ tr ( "Hero %1 has duplicate on map" ). arg ( ins -> getHeroType () -> getNameTranslated (). c_str ()), false });
2022-09-18 03:23:17 +04:00
}
2023-09-10 16:57:41 +02:00
else if ( ins -> ID != Obj :: RANDOM_HERO )
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Hero %1 has an empty type and must be removed" ). arg ( ins -> instanceName . c_str ()), true });
2022-09-18 03:23:17 +04:00
}
//checking for arts
2024-08-20 00:53:48 +02:00
if ( auto * ins = dynamic_cast < CGArtifact *> ( o . get ()))
2022-09-18 03:23:17 +04:00
{
if ( ins -> ID == Obj :: SPELL_SCROLL )
{
2025-03-18 20:38:12 +00:00
if ( ins -> getArtifactInstance ())
2022-09-18 03:23:17 +04:00
{
2025-03-18 20:38:12 +00:00
if ( map -> allowedSpells . count ( ins -> getArtifactInstance () -> getScrollSpellID ()) == 0 )
issues . insert ({ tr ( "Spell scroll %1 is prohibited by map settings" ). arg ( ins -> getArtifactInstance () -> getScrollSpellID (). toEntity ( LIBRARY -> spells ()) -> getNameTranslated (). c_str ()), false });
2022-09-18 03:23:17 +04:00
}
else
2024-09-13 15:02:09 +03:00
issues . insert ({ tr ( "Spell scroll %1 doesn't have instance assigned and must be removed" ). arg ( ins -> instanceName . c_str ()), true });
2022-09-18 03:23:17 +04:00
}
else
{
2025-03-18 20:38:12 +00:00
if ( ins -> ID == Obj :: ARTIFACT && map -> allowedArtifact . count ( ins -> getArtifactType ()) == 0 )
2022-09-18 03:23:17 +04:00
{
2024-09-13 15:02:09 +03:00
issues . insert ({ tr ( "Artifact %1 is prohibited by map settings" ). arg ( ins -> getObjectName (). c_str ()), false });
2022-09-18 03:23:17 +04:00
}
}
}
}
//verification of starting towns
2024-08-20 00:53:48 +02:00
for ( const auto & [ player , counter ] : amountOfTowns )
{
if ( counter == 0 )
{
// FIXME: heroesNames are empty even though heroes are on the map
// if(map->players[playerTCounter.first].heroesNames.empty())
if ( amountOfHeroes . count ( player ) == 0 )
issues . insert ({ tr ( "Player %1 has no towns and heroes assigned" ). arg ( player + 1 ), true });
else
issues . insert ({ tr ( "Player %1 doesn't have any starting town" ). arg ( player + 1 ), false });
}
}
2022-09-18 03:23:17 +04:00
//verification of map name and description
if ( map -> name . empty ())
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Map name is not specified" ), false });
2022-09-18 03:23:17 +04:00
if ( map -> description . empty ())
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Map description is not specified" ), false });
2023-04-17 03:01:29 +04:00
2025-02-09 19:19:36 +01:00
//verification for mods
2023-04-17 03:01:29 +04:00
for ( auto & mod : MapController :: modAssessmentMap ( * map ))
{
if ( ! map -> mods . count ( mod . first ))
2025-05-13 12:51:31 +02:00
issues . insert ({ MapController :: modMissingMessage ( mod . second ), true });
2023-04-17 03:01:29 +04:00
}
2022-09-18 03:23:17 +04:00
}
catch ( const std :: exception & e )
{
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Exception occurs during validation: %1" ). arg ( e . what ()), true });
2022-09-18 03:23:17 +04:00
}
catch (...)
{
2024-08-20 00:53:48 +02:00
issues . insert ({ tr ( "Unknown exception occurs during validation" ), true });
2022-09-18 03:23:17 +04:00
}
return issues ;
}
2025-02-09 19:19:36 +01:00
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 ))
{
2025-04-16 23:44:17 +02:00
auto * item = new QListWidgetItem ( QIcon ( issue . critical ? ":/icons/mod-delete.png" : ":/icons/mod-update.png" ),
issue . message , ui -> listWidget );
2025-02-09 19:19:36 +01:00
ui -> listWidget -> addItem ( item );
}
if ( ui -> listWidget -> count () == 0 )
{
2025-05-13 12:51:31 +02:00
QPixmap greenTick ( ":/icons/mod-enabled.png" );
2025-02-09 19:19:36 +01:00
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 )
{
2025-04-05 18:13:24 +02:00
parentWidget -> setMinimumWidth ( finalWidth + padding );
parentWidget -> setMinimumHeight ( finalHeight + padding );
2025-02-09 19:19:36 +01:00
}
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 );
}