diff --git a/config/schemas/settings.json b/config/schemas/settings.json
index da312e19d..276903873 100644
--- a/config/schemas/settings.json
+++ b/config/schemas/settings.json
@@ -3,7 +3,7 @@
{
"type" : "object",
"$schema" : "http://json-schema.org/draft-04/schema",
- "required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "lobby", "gameTweaks" ],
+ "required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "lobby", "gameTweaks", "mods" ],
"definitions" : {
"logLevelEnum" : {
"type" : "string",
@@ -149,6 +149,23 @@
}
}
},
+
+ "mods" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "default" : {},
+ "required" : [
+ "validation"
+ ],
+ "properties" : {
+ "validation" : {
+ "type" : "string",
+ "enum" : [ "off", "basic", "full" ],
+ "default" : "basic"
+ }
+ }
+ },
+
"video" : {
"type" : "object",
"additionalProperties" : false,
diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp
index fd897d1f5..91788e7b6 100644
--- a/launcher/settingsView/csettingsview_moc.cpp
+++ b/launcher/settingsView/csettingsview_moc.cpp
@@ -182,6 +182,13 @@ void CSettingsView::loadSettings()
else
ui->buttonFontScalable->setChecked(true);
+ if (settings["mods"]["validation"].String() == "off")
+ ui->buttonValidationOff->setChecked(true);
+ else if (settings["mods"]["validation"].String() == "basic")
+ ui->buttonValidationBasic->setChecked(true);
+ else
+ ui->buttonValidationFull->setChecked(true);
+
loadToggleButtonSettings();
}
@@ -791,3 +798,21 @@ void CSettingsView::on_buttonFontOriginal_clicked(bool checked)
Settings node = settings.write["video"]["fontsType"];
node->String() = "original";
}
+
+void CSettingsView::on_buttonValidationOff_clicked(bool checked)
+{
+ Settings node = settings.write["mods"]["validation"];
+ node->String() = "off";
+}
+
+void CSettingsView::on_buttonValidationBasic_clicked(bool checked)
+{
+ Settings node = settings.write["mods"]["validation"];
+ node->String() = "basic";
+}
+
+void CSettingsView::on_buttonValidationFull_clicked(bool checked)
+{
+ Settings node = settings.write["mods"]["validation"];
+ node->String() = "full";
+}
diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h
index 1e76f6f2e..512762d0c 100644
--- a/launcher/settingsView/csettingsview_moc.h
+++ b/launcher/settingsView/csettingsview_moc.h
@@ -83,19 +83,20 @@ private slots:
void on_sliderToleranceDistanceController_valueChanged(int value);
void on_lineEditGameLobbyHost_textChanged(const QString &arg1);
void on_spinBoxNetworkPortLobby_valueChanged(int arg1);
-
void on_sliderControllerSticksAcceleration_valueChanged(int value);
-
void on_sliderControllerSticksSensitivity_valueChanged(int value);
-
- //void on_buttonTtfFont_toggled(bool value);
-
void on_sliderScalingFont_valueChanged(int value);
-
void on_buttonFontAuto_clicked(bool checked);
void on_buttonFontScalable_clicked(bool checked);
void on_buttonFontOriginal_clicked(bool checked);
+
+ void on_buttonValidationOff_clicked(bool checked);
+
+ void on_buttonValidationBasic_clicked(bool checked);
+
+ void on_buttonValidationFull_clicked(bool checked);
+
private:
Ui::CSettingsView * ui;
diff --git a/launcher/settingsView/csettingsview_moc.ui b/launcher/settingsView/csettingsview_moc.ui
index 578fd55b4..c0082edd8 100644
--- a/launcher/settingsView/csettingsview_moc.ui
+++ b/launcher/settingsView/csettingsview_moc.ui
@@ -47,88 +47,19 @@
0
- 0
+ -800
729
- 1449
+ 1506
- -
-
-
- false
-
-
- BattleAI
-
-
-
-
- BattleAI
-
-
- -
-
- StupidAI
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
+
-
+
-
- true
-
- -
-
-
- Use Relative Pointer Mode
-
-
-
- -
-
-
- 25
-
-
- Qt::Horizontal
-
-
- QSlider::TicksAbove
-
-
- 5
-
-
-
- -
-
-
- Additional repository
-
-
-
- -
-
-
- Music Volume
-
-
-
- -
-
-
-
@@ -157,374 +88,6 @@
- -
-
-
- Long Touch Duration
-
-
-
- -
-
-
- false
-
-
- BattleAI
-
-
-
-
- BattleAI
-
-
- -
-
- StupidAI
-
-
-
-
- -
-
-
-
- true
-
-
-
- Video
-
-
- 5
-
-
-
- -
-
-
-
- true
-
-
-
- General
-
-
- 5
-
-
-
- -
-
-
- Heroes III Translation
-
-
-
- -
-
-
- Show Tutorial again
-
-
-
- -
-
-
- Online Lobby port
-
-
-
- -
-
-
- Relative Pointer Speed
-
-
-
- -
-
-
- Touch Tap Tolerance
-
-
-
- -
-
-
- Select display mode for game
-
-Windowed - game will run inside a window that covers part of your screen
-
-Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen.
-
-Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution.
-
-
- 0
-
-
-
-
- Windowed
-
-
- -
-
- Borderless fullscreen
-
-
- -
-
- Exclusive fullscreen
-
-
-
-
- -
-
-
- 500
-
-
- 2000
-
-
- 250
-
-
- 250
-
-
- Qt::Horizontal
-
-
- QSlider::TicksAbove
-
-
- 250
-
-
-
- -
-
-
- Neutral AI in battles
-
-
-
- -
-
-
-
- true
-
-
-
- Input - Mouse
-
-
- 5
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Automatic
-
-
- true
-
-
- true
-
-
- buttonGroup
-
-
-
- -
-
-
- Adventure Map Enemies
-
-
-
- -
-
-
- VCAI
-
-
-
-
- VCAI
-
-
- -
-
- Nullkiller
-
-
-
-
- -
-
-
- 0
-
-
- 50
-
-
- 1
-
-
- 10
-
-
- 0
-
-
- Qt::Horizontal
-
-
- QSlider::TicksAbove
-
-
- 10
-
-
-
- -
-
-
- Autosave
-
-
-
- -
-
-
- Renderer
-
-
-
- -
-
-
- Autosave limit (0 = off)
-
-
-
- -
-
-
- Default repository
-
-
-
- -
-
-
- 100
-
-
- Qt::Horizontal
-
-
- QSlider::TicksAbove
-
-
- 10
-
-
-
- -
-
-
- Sticks Sensitivity
-
-
-
- -
-
-
-
- true
-
-
-
- Audio
-
-
- 5
-
-
-
- -
-
-
- -
-
-
- Downscaling Filter
-
-
-
- -
-
-
- 1024
-
-
- 65535
-
-
- 3030
-
-
-
- -
-
-
- 0
-
-
- 50
-
-
- 1
-
-
- 10
-
-
- 0
-
-
- Qt::Horizontal
-
-
- QSlider::TicksAbove
-
-
- 10
-
-
-
- -
-
-
- Ignore SSL errors
-
-
-
-
@@ -532,120 +95,6 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
-
- Refresh now
-
-
-
- -
-
-
- true
-
-
-
- 0
- 0
-
-
-
-
-
-
- true
-
-
- false
-
-
-
- -
-
-
- VSync
-
-
-
- -
-
-
- Resolution
-
-
-
- -
-
-
- VCMI Language
-
-
-
- -
-
-
- Interface Scaling
-
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
-
-
- Online Lobby address
-
-
-
- -
-
-
- VCAI
-
-
-
-
- VCAI
-
-
- -
-
- Nullkiller
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
-
-
- true
-
-
-
- -
-
-
-
-
-
-
-
@@ -674,38 +123,37 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
+
-
+
+
+ false
+
+
+ BattleAI
+
+
-
+
+ BattleAI
+
+
+ -
+
+ StupidAI
+
+
+
+
+ -
+
- Autosave prefix
+ Additional repository
- -
-
+
-
+
- Reserved screen area
-
-
-
- -
-
-
- Sound Volume
-
-
-
- -
-
-
- Font Scaling (experimental)
-
-
-
- -
-
-
- Framerate Limit
+ Ignore SSL errors
@@ -728,8 +176,130 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
+
-
+
+
+ VCMI Language
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Automatic
+
+
+ true
+
+
+ true
+
+
+ buttonGroupFonts
+
+
+
+ -
+
+
+
+ true
+
+
+
+ Video
+
+
+ 5
+
+
+
+ -
+
+
+ Show intro
+
+
+
+ -
+
+
+ Heroes III Translation
+
+
+
+ -
+
+
+ 0
+
+
+ 50
+
+
+ 1
+
+
+ 10
+
+
+ 0
+
+
+ Qt::Horizontal
+
+
+ QSlider::TicksAbove
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Scalable
+
+
+ true
+
+
+ true
+
+
+ buttonGroupFonts
+
+
+
+ -
+
+
+ Touch Tap Tolerance
+
+
+
+ -
+
+
+
+
+
+
+ -
+
100
@@ -744,56 +314,13 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
-
-
-
- Automatic
-
-
- -
-
- None
-
-
- -
-
- xBRZ x2
-
-
- -
-
- xBRZ x3
-
-
- -
-
- xBRZ x4
-
-
-
+ -
+
- -
-
+
-
+
- Autocombat AI in battles
-
-
-
- -
-
-
- Use scalable fonts
-
-
-
- -
-
-
-
-
-
- true
+ Renderer
@@ -812,87 +339,24 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
+
-
+
- Fullscreen
+ Neutral AI in battles
- -
-
-
- 500
-
-
- 2500
-
-
- 25
-
-
- 250
-
-
- 500
-
-
- Qt::Horizontal
-
-
- QSlider::TicksAbove
-
-
- 250
-
-
-
- -
-
+
-
+
- Enemy AI in battles
+ VSync
- -
-
+
-
+
- Sticks Acceleration
-
-
-
- -
-
-
- 100
-
-
- 300
-
-
- Qt::Horizontal
-
-
- QSlider::TicksAbove
-
-
- 25
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
-
-
- true
+ Framerate Limit
@@ -903,8 +367,61 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
+
-
+
+
+
+ true
+
+
+
+ Input - Touchscreen
+
+
+ 5
+
+
+
+ -
+
+
+ 0
+
+
+ 50
+
+
+ 1
+
+
+ 10
+
+
+ 0
+
+
+ Qt::Horizontal
+
+
+ QSlider::TicksAbove
+
+
+ 10
+
+
+
+ -
+
+
+ Relative Pointer Speed
+
+
+
+ -
+
+
+ -
+
true
@@ -925,29 +442,6 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
-
-
- 0
- 0
-
-
-
-
-
-
- true
-
-
-
- -
-
-
-
-
-
-
-
@@ -964,33 +458,20 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
+
-
+
- Adventure Map Allies
+ Long Touch Duration
- -
-
-
- Haptic Feedback
-
-
+
-
+
- -
-
-
- %
-
-
- 50
-
-
- 400
-
-
- 10
+
-
+
+
+ Check on startup
@@ -1013,72 +494,6 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
-
- Mouse Click Tolerance
-
-
-
- -
-
-
- BattleAI
-
-
-
-
- BattleAI
-
-
- -
-
- StupidAI
-
-
-
-
- -
-
-
- true
-
-
-
- 0
- 0
-
-
-
-
-
-
- true
-
-
- false
-
-
-
- -
-
-
- Reset
-
-
-
- -
-
-
- -
-
-
- Network port
-
-
-
- -
-
-
-
@@ -1094,15 +509,8 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
-
- Display index
-
-
-
- -
-
+
-
+
0
@@ -1117,128 +525,6 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
-
-
- true
-
-
-
- Network
-
-
- 5
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Original
-
-
- true
-
-
- true
-
-
- buttonGroup
-
-
-
- -
-
-
-
-
-
-
- -
-
-
- Show intro
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
-
-
- true
-
-
-
- -
-
-
- 1024
-
-
- 65535
-
-
- 3030
-
-
-
- -
-
-
-
- true
-
-
-
- Input - Touchscreen
-
-
- 5
-
-
-
- -
-
-
- 20
-
-
- 1000
-
-
- 10
-
-
-
- -
-
-
- Controller Click Tolerance
-
-
-
- -
-
-
- Check on startup
-
-
-
-
@@ -1270,15 +556,53 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- -
-
+
-
+
- Software Cursor
+ Downscaling Filter
- -
-
+
-
+
+
+ Select display mode for game
+
+Windowed - game will run inside a window that covers part of your screen
+
+Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen.
+
+Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution.
+
+
+ 0
+
+
-
+
+ Windowed
+
+
+ -
+
+ Borderless fullscreen
+
+
+ -
+
+ Exclusive fullscreen
+
+
+
+
+ -
+
+
+ Use scalable fonts
+
+
+
+ -
+
0
@@ -1286,7 +610,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- Scalable
+ Original
true
@@ -1295,13 +619,786 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
true
- buttonGroup
+ buttonGroupFonts
+ -
+
+
+ -
+
+
-
+
+ Automatic
+
+
+ -
+
+ None
+
+
+ -
+
+ xBRZ x2
+
+
+ -
+
+ xBRZ x3
+
+
+ -
+
+ xBRZ x4
+
+
+
+
+ -
+
+
+ Online Lobby port
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ VCAI
+
+
-
+
+ VCAI
+
+
+ -
+
+ Nullkiller
+
+
+
+
+ -
+
+
+
+ true
+
+
+
+ Network
+
+
+ 5
+
+
+
+ -
+
+
+ Refresh now
+
+
+
+ -
+
+
+ 500
+
+
+ 2500
+
+
+ 25
+
+
+ 250
+
+
+ 500
+
+
+ Qt::Horizontal
+
+
+ QSlider::TicksAbove
+
+
+ 250
+
+
+
+ -
+
+
+ Display index
+
+
+
+ -
+
+
+ 500
+
+
+ 2000
+
+
+ 250
+
+
+ 250
+
+
+ Qt::Horizontal
+
+
+ QSlider::TicksAbove
+
+
+ 250
+
+
+
+ -
+
+
+ false
+
+
+ BattleAI
+
+
-
+
+ BattleAI
+
+
+ -
+
+ StupidAI
+
+
+
+
+ -
+
+
+ Adventure Map Enemies
+
+
+
+ -
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ Reset
+
+
+
+ -
+
+
+ Show Tutorial again
+
+
+
+ -
+
+
+ Fullscreen
+
+
+
+ -
+
+
+ Use Relative Pointer Mode
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Sticks Acceleration
+
+
+
+ -
+
+
+ Autosave limit (0 = off)
+
+
+
+ -
+
+
+ Mouse Click Tolerance
+
+
+
+ -
+
+
+
+ true
+
+
+
+ General
+
+
+ 5
+
+
+
+ -
+
+
+ Online Lobby address
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ Interface Scaling
+
+
+
+ -
+
+
+ Default repository
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ Haptic Feedback
+
+
+
+ -
+
+
+ -
+
+
+ Autosave prefix
+
+
+
+ -
+
+
+
+ true
+
+
+
+ Input - Mouse
+
+
+ 5
+
+
+
+ -
+
+
+ 25
+
+
+ Qt::Horizontal
+
+
+ QSlider::TicksAbove
+
+
+ 5
+
+
+
+ -
+
+
+ 20
+
+
+ 1000
+
+
+ 10
+
+
+
+ -
+
+
+ 1024
+
+
+ 65535
+
+
+ 3030
+
+
+
+ -
+
+
+ 100
+
+
+ 300
+
+
+ Qt::Horizontal
+
+
+ QSlider::TicksAbove
+
+
+ 25
+
+
+
+ -
+
+
+ Enemy AI in battles
+
+
+
+ -
+
+
+ Reserved screen area
+
+
+
+ -
+
+
+ %
+
+
+ 50
+
+
+ 400
+
+
+ 10
+
+
+
+ -
+
+
+ Music Volume
+
+
+
+ -
+
+
+ BattleAI
+
+
-
+
+ BattleAI
+
+
+ -
+
+ StupidAI
+
+
+
+
+ -
+
+
+ Autosave
+
+
+
+ -
+
+
+ 100
+
+
+ Qt::Horizontal
+
+
+ QSlider::TicksAbove
+
+
+ 10
+
+
+
+ -
+
+
+ Network port
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Resolution
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ Sound Volume
+
+
+
+ -
+
+
+ Controller Click Tolerance
+
+
+
+ -
+
+
+ Adventure Map Allies
+
+
+
+ -
+
+
+
+ true
+
+
+
+ Miscellaneous
+
+
+ 5
+
+
+
+ -
+
+
+ 1024
+
+
+ 65535
+
+
+ 3030
+
+
+
+ -
+
+
+ VCAI
+
+
-
+
+ VCAI
+
+
+ -
+
+ Nullkiller
+
+
+
+
+ -
+
+
+
+ true
+
+
+
+ Audio
+
+
+ 5
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ Software Cursor
+
+
+
+ -
+
+
+ Autocombat AI in battles
+
+
+
-
+ -
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Font Scaling (experimental)
+
+
+
+ -
+
+
+ Sticks Sensitivity
+
+
+
+ -
+
+
+ Mods Validation
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+ Off
+
+
+ true
+
+
+ false
+
+
+ buttonGroupValidation
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+ Basic
+
+
+ true
+
+
+ false
+
+
+ buttonGroupValidation
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+ Full
+
+
+ true
+
+
+ false
+
+
+ buttonGroupValidation
+
+
+
@@ -1311,6 +1408,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
-
+
+
diff --git a/lib/json/JsonUtils.cpp b/lib/json/JsonUtils.cpp
index e1cd47678..6ede7be82 100644
--- a/lib/json/JsonUtils.cpp
+++ b/lib/json/JsonUtils.cpp
@@ -296,4 +296,28 @@ JsonNode JsonUtils::assembleFromFiles(const std::string & filename)
return result;
}
+void JsonUtils::detectConflicts(JsonNode & result, const JsonNode & left, const JsonNode & right, const std::string & keyName)
+{
+ switch (left.getType())
+ {
+ case JsonNode::JsonType::DATA_NULL:
+ case JsonNode::JsonType::DATA_BOOL:
+ case JsonNode::JsonType::DATA_FLOAT:
+ case JsonNode::JsonType::DATA_INTEGER:
+ case JsonNode::JsonType::DATA_STRING:
+ case JsonNode::JsonType::DATA_VECTOR: // NOTE: comparing vectors as whole - since merge will overwrite it in its entirety
+ {
+ result[keyName][left.getModScope()] = left;
+ result[keyName][right.getModScope()] = right;
+ return;
+ }
+ case JsonNode::JsonType::DATA_STRUCT:
+ {
+ for(const auto & node : left.Struct())
+ if (!right[node.first].isNull())
+ detectConflicts(result, node.second, right[node.first], keyName + "/" + node.first);
+ }
+ }
+}
+
VCMI_LIB_NAMESPACE_END
diff --git a/lib/json/JsonUtils.h b/lib/json/JsonUtils.h
index dcf03d30e..85c322bcb 100644
--- a/lib/json/JsonUtils.h
+++ b/lib/json/JsonUtils.h
@@ -74,6 +74,12 @@ namespace JsonUtils
/// get schema by json URI: vcmi:#
/// example: schema "vcmi:settings" is used to check user settings
DLL_LINKAGE const JsonNode & getSchema(const std::string & URI);
+
+ /// detects potential conflicts - json entries present in both nodes
+ /// returns JsonNode that contains list of conflicting keys
+ /// For each conflict - list of conflicting mods and list of conflicting json values
+ /// result[pathToKey][modID] -> node that was conflicting
+ DLL_LINKAGE void detectConflicts(JsonNode & result, const JsonNode & left, const JsonNode & right, const std::string & keyName);
}
VCMI_LIB_NAMESPACE_END
diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp
index 20375961a..ed0c92142 100644
--- a/lib/mapObjectConstructors/CRewardableConstructor.cpp
+++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp
@@ -14,6 +14,7 @@
#include "../mapObjects/CRewardableObject.h"
#include "../texts/CGeneralTextHandler.h"
#include "../IGameCallback.h"
+#include "../CConfigHandler.h"
VCMI_LIB_NAMESPACE_BEGIN
@@ -25,7 +26,8 @@ void CRewardableConstructor::initTypeData(const JsonNode & config)
if (!config["name"].isNull())
VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"]);
- JsonUtils::validate(config, "vcmi:rewardable", getJsonKey());
+ if (settings["mods"]["validation"].String() != "off")
+ JsonUtils::validate(config, "vcmi:rewardable", getJsonKey());
}
diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp
index 8dbf84eda..2f2780c5b 100644
--- a/lib/modding/CModHandler.cpp
+++ b/lib/modding/CModHandler.cpp
@@ -17,6 +17,7 @@
#include "ModIncompatibility.h"
#include "../CCreatureHandler.h"
+#include "../CConfigHandler.h"
#include "../CStopWatch.h"
#include "../GameSettings.h"
#include "../ScriptHandler.h"
@@ -331,10 +332,38 @@ void CModHandler::loadModFilesystems()
coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin())));
+ std::map modFilesystems;
+
for(std::string & modName : activeMods)
+ modFilesystems[modName] = genModFilesystem(modName, allMods[modName].config);
+
+ for(std::string & modName : activeMods)
+ CResourceHandler::addFilesystem("data", modName, modFilesystems[modName]);
+
+ if (settings["mods"]["validation"].String() == "full")
{
- CModInfo & mod = allMods[modName];
- CResourceHandler::addFilesystem("data", modName, genModFilesystem(modName, mod.config));
+ for(std::string & leftModName : activeMods)
+ {
+ for(std::string & rightModName : activeMods)
+ {
+ if (leftModName == rightModName)
+ continue;
+
+ if (getModDependencies(leftModName).count(rightModName) || getModDependencies(rightModName).count(leftModName))
+ continue;
+
+ const auto & filter = [](const ResourcePath &path){return path.getType() != EResType::DIRECTORY;};
+
+ std::unordered_set leftResources = modFilesystems[leftModName]->getFilteredFiles(filter);
+ std::unordered_set rightResources = modFilesystems[rightModName]->getFilteredFiles(filter);
+
+ for (auto const & leftFile : leftResources)
+ {
+ if (rightResources.count(leftFile))
+ logMod->warn("Potential confict detected between '%s' and '%s': both mods add file '%s'", leftModName, rightModName, leftFile.getOriginalName());
+ }
+ }
+ }
}
}
@@ -370,6 +399,12 @@ std::string CModHandler::getModLanguage(const TModID& modId) const
return allMods.at(modId).baseLanguage;
}
+std::set CModHandler::getModDependencies(const TModID & modId) const
+{
+ bool isModFound;
+ return getModDependencies(modId, isModFound);
+}
+
std::set CModHandler::getModDependencies(const TModID & modId, bool & isModFound) const
{
auto it = allMods.find(modId);
diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h
index 358483c22..fab2d319b 100644
--- a/lib/modding/CModHandler.h
+++ b/lib/modding/CModHandler.h
@@ -64,6 +64,7 @@ public:
std::string getModLanguage(const TModID & modId) const;
+ std::set getModDependencies(const TModID & modId) const;
std::set getModDependencies(const TModID & modId, bool & isModFound) const;
/// returns list of all (active) mods
diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp
index d5dfb8461..0a9acb0f4 100644
--- a/lib/modding/ContentTypeHandler.cpp
+++ b/lib/modding/ContentTypeHandler.cpp
@@ -17,6 +17,7 @@
#include "../BattleFieldHandler.h"
#include "../CArtHandler.h"
#include "../CCreatureHandler.h"
+#include "../CConfigHandler.h"
#include "../entities/faction/CTownHandler.h"
#include "../texts/CGeneralTextHandler.h"
#include "../CHeroHandler.h"
@@ -39,9 +40,9 @@
VCMI_LIB_NAMESPACE_BEGIN
-ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & objectName):
+ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & entityName):
handler(handler),
- objectName(objectName),
+ entityName(entityName),
originalData(handler->loadLegacyData())
{
for(auto & node : originalData)
@@ -79,6 +80,9 @@ bool ContentTypeHandler::preloadModData(const std::string & modName, const JsonN
logMod->trace("Patching object %s (%s) from %s", objectName, remoteName, modName);
JsonNode & remoteConf = modData[remoteName].patches[objectName];
+ if (!remoteConf.isNull() && settings["mods"]["validation"].String() != "off")
+ JsonUtils::detectConflicts(conflictList, remoteConf, entry.second, objectName);
+
JsonUtils::merge(remoteConf, entry.second);
}
}
@@ -93,7 +97,7 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate)
auto performValidate = [&,this](JsonNode & data, const std::string & name){
handler->beforeValidate(data);
if (validate)
- result &= JsonUtils::validate(data, "vcmi:" + objectName, name);
+ result &= JsonUtils::validate(data, "vcmi:" + entityName, name);
};
// apply patches
@@ -113,7 +117,7 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate)
// - another mod attempts to add object into this mod (technically can be supported, but might lead to weird edge cases)
// - another mod attempts to edit object from this mod that no longer exist - DANGER since such patch likely has very incomplete data
// so emit warning and skip such case
- logMod->warn("Mod '%s' attempts to edit object '%s' of type '%s' from mod '%s' but no such object exist!", data.getModScope(), name, objectName, modName);
+ logMod->warn("Mod '%s' attempts to edit object '%s' of type '%s' from mod '%s' but no such object exist!", data.getModScope(), name, entityName, modName);
continue;
}
@@ -159,31 +163,70 @@ void ContentTypeHandler::loadCustom()
void ContentTypeHandler::afterLoadFinalization()
{
- for (auto const & data : modData)
+ if (settings["mods"]["validation"].String() != "off")
{
- if (data.second.modData.isNull())
+ for (auto const & data : modData)
{
- for (auto node : data.second.patches.Struct())
- logMod->warn("Mod '%s' have added patch for object '%s' from mod '%s', but this mod was not loaded or has no new objects.", node.second.getModScope(), node.first, data.first);
- }
-
- for(auto & otherMod : modData)
- {
- if (otherMod.first == data.first)
- continue;
-
- if (otherMod.second.modData.isNull())
- continue;
-
- for(auto & otherObject : otherMod.second.modData.Struct())
+ if (data.second.modData.isNull())
{
- if (data.second.modData.Struct().count(otherObject.first))
+ for (auto node : data.second.patches.Struct())
+ logMod->warn("Mod '%s' have added patch for object '%s' from mod '%s', but this mod was not loaded or has no new objects.", node.second.getModScope(), node.first, data.first);
+ }
+
+ for(auto & otherMod : modData)
+ {
+ if (otherMod.first == data.first)
+ continue;
+
+ if (otherMod.second.modData.isNull())
+ continue;
+
+ for(auto & otherObject : otherMod.second.modData.Struct())
{
- logMod->warn("Mod '%s' have added object with name '%s' that is also available in mod '%s'", data.first, otherObject.first, otherMod.first);
- logMod->warn("Two objects with same name were loaded. Please use form '%s:%s' if mod '%s' needs to modify this object instead", otherMod.first, otherObject.first, data.first);
+ if (data.second.modData.Struct().count(otherObject.first))
+ {
+ logMod->warn("Mod '%s' have added object with name '%s' that is also available in mod '%s'", data.first, otherObject.first, otherMod.first);
+ logMod->warn("Two objects with same name were loaded. Please use form '%s:%s' if mod '%s' needs to modify this object instead", otherMod.first, otherObject.first, data.first);
+ }
}
}
}
+
+ for (const auto& [conflictPath, conflictModData] : conflictList.Struct())
+ {
+ std::set conflictingMods;
+ std::set resolvedConflicts;
+
+ for (auto const & conflictModData : conflictModData.Struct())
+ conflictingMods.insert(conflictModData.first);
+
+ for (auto const & modID : conflictingMods)
+ resolvedConflicts.merge(VLC->modh->getModDependencies(modID));
+
+ vstd::erase_if(conflictingMods, [&resolvedConflicts](const std::string & entry){ return resolvedConflicts.count(entry);});
+
+ if (conflictingMods.size() < 2)
+ continue; // all conflicts were resolved - either via compatibility patch (mod that depends on 2 conflicting mods) or simple mod that depends on another one
+
+ bool allEqual = true;
+
+ for (auto const & modID : conflictingMods)
+ {
+ if (conflictModData[modID] != conflictModData[*conflictingMods.begin()])
+ {
+ allEqual = false;
+ break;
+ }
+ }
+
+ if (allEqual)
+ continue; // conflict still present, but all mods use the same value for conflicting entry - permit it
+
+ logMod->warn("Potential confict in '%s'", conflictPath);
+
+ for (auto const & modID : conflictingMods)
+ logMod->warn("Mod '%s' - value set to %s", modID, conflictModData[modID].toCompactString());
+ }
}
handler->afterLoadFinalization();
@@ -249,7 +292,7 @@ void CContentHandler::afterLoadFinalization()
void CContentHandler::preloadData(CModInfo & mod)
{
- bool validate = (mod.validation != CModInfo::PASSED);
+ bool validate = validateMod(mod);
// print message in format [<8-symbols checksum>]
auto & info = mod.getVerificationInfo();
@@ -266,7 +309,7 @@ void CContentHandler::preloadData(CModInfo & mod)
void CContentHandler::load(CModInfo & mod)
{
- bool validate = (mod.validation != CModInfo::PASSED);
+ bool validate = validateMod(mod);
if (!loadMod(mod.identifier, validate))
mod.validation = CModInfo::FAILED;
@@ -287,4 +330,18 @@ const ContentTypeHandler & CContentHandler::operator[](const std::string & name)
return handlers.at(name);
}
+bool CContentHandler::validateMod(const CModInfo & mod) const
+{
+ if (settings["mods"]["validation"].String() == "full")
+ return true;
+
+ if (mod.validation == CModInfo::PASSED)
+ return false;
+
+ if (settings["mods"]["validation"].String() == "off")
+ return false;
+
+ return true;
+}
+
VCMI_LIB_NAMESPACE_END
diff --git a/lib/modding/ContentTypeHandler.h b/lib/modding/ContentTypeHandler.h
index 60b29d689..a623cb226 100644
--- a/lib/modding/ContentTypeHandler.h
+++ b/lib/modding/ContentTypeHandler.h
@@ -19,6 +19,8 @@ class CModInfo;
/// internal type to handle loading of one data type (e.g. artifacts, creatures)
class DLL_LINKAGE ContentTypeHandler
{
+ JsonNode conflictList;
+
public:
struct ModInfo
{
@@ -29,7 +31,7 @@ public:
};
/// handler to which all data will be loaded
IHandlerBase * handler;
- std::string objectName;
+ std::string entityName;
/// contains all loaded H3 data
std::vector originalData;
@@ -56,6 +58,7 @@ class DLL_LINKAGE CContentHandler
std::map handlers;
+ bool validateMod(const CModInfo & mod) const;
public:
void init();