From c927913f5f153db44671e5d3692173abc4986a07 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 2 Apr 2013 17:06:43 +0000 Subject: [PATCH] - Improved json validation. Now it support most of features from latest json schema draft - Set of schemas in config/schemas directory that are used to validate input from mods. --- client/CMT.cpp | 2 +- config/defaultSettings.json | 126 ------ config/factions/castle.json | 3 +- config/schemas/artifact.json | 82 ++++ config/schemas/bonus.json | 92 +++++ config/schemas/creature.json | 244 ++++++++++++ config/schemas/faction.json | 250 ++++++++++++ config/schemas/hero.json | 151 +++++++ config/schemas/heroClass.json | 92 +++++ config/schemas/mod.json | 78 ++++ config/schemas/settings.json | 133 +++++++ config/schemas/townBuilding.json | 50 +++ config/schemas/townSiege.json | 349 ++++++++++++++++ config/schemas/townStructure.json | 45 +++ lib/CArtHandler.cpp | 4 +- lib/CConfigHandler.cpp | 9 +- lib/CConsoleHandler.cpp | 6 +- lib/CModHandler.cpp | 31 +- lib/CModHandler.h | 7 +- lib/CObjectHandler.cpp | 2 + lib/Filesystem/CResourceLoader.cpp | 3 +- lib/JsonNode.cpp | 616 ++++++++++++++++++++++------- lib/JsonNode.h | 100 +++-- lib/ScopeGuard.h | 47 +++ lib/VCMI_Lib.cpp | 3 +- 25 files changed, 2186 insertions(+), 339 deletions(-) delete mode 100644 config/defaultSettings.json create mode 100644 config/schemas/artifact.json create mode 100644 config/schemas/bonus.json create mode 100644 config/schemas/creature.json create mode 100644 config/schemas/faction.json create mode 100644 config/schemas/hero.json create mode 100644 config/schemas/heroClass.json create mode 100644 config/schemas/mod.json create mode 100644 config/schemas/settings.json create mode 100644 config/schemas/townBuilding.json create mode 100644 config/schemas/townSiege.json create mode 100644 config/schemas/townStructure.json create mode 100644 lib/ScopeGuard.h diff --git a/client/CMT.cpp b/client/CMT.cpp index 7a0e10a80..c15349e98 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -308,7 +308,7 @@ int main(int argc, char** argv) tlog0 << "Fatal error: failed to load settings!\n"; tlog0 << "Possible reasons:\n"; tlog0 << "\tCorrupted local configuration file at " << VCMIDirs::get().localPath() << "/config/settings.json\n"; - tlog0 << "\tMissing or corrupted global configuration file at " << VCMIDirs::get().dataPath() << "/config/defaultSettings.json\n"; + tlog0 << "\tMissing or corrupted global configuration file at " << VCMIDirs::get().dataPath() << "/config/schemas/settings.json\n"; tlog0 << "VCMI will now exit...\n"; exit(EXIT_FAILURE); } diff --git a/config/defaultSettings.json b/config/defaultSettings.json deleted file mode 100644 index 99279cf07..000000000 --- a/config/defaultSettings.json +++ /dev/null @@ -1,126 +0,0 @@ -// This is schema for checking game settings from settings.json -// Any new settings should be added in this file for correct serialization and initialization -{ - "general" : { - "type" : "object", - "properties" : { - "classicCreatureWindow" : { - "type" : "bool", - "default" : false - }, - "playerName" : { - "type":"string", - "default" : "Player" - }, - "showfps" : { - "type" : "bool", - "default" : false - }, - "music" : { - "type" : "number", - "default" : 88 - }, - "sound" : { - "type" : "number", - "default" : 88 - } - }, - "default" : {} - }, - "video" : { - "type" : "object", - "properties" : { - "screenRes" : { - "type" : "object", - "properties" : { - "width" : { "type" : "number" }, - "height" : { "type" : "number" } - }, - "default": {"width" : 800, "height": 600 } - }, - "bitsPerPixel" : { - "type" : "number", - "default" : 24 - }, - "fullscreen" : { - "type" : "bool", - "default" : false - } - }, - "default" : {} - }, - "adventure" : { - "type" : "object", - "properties" : { - "heroSpeed" : { - "type" : "number", - "default" : 2 - }, - "enemySpeed" : { - "type" : "number", - "default" : 2 - }, - "scrollSpeed" : { - "type" : "number", - "default" : 1 - }, - "heroReminder" : { - "type" : "bool", - "default" : true - } - }, - "default" : {} - }, - "battle" : { - "type" : "object", - "properties" : { - "animationSpeed" : { - "type" : "number", - "default" : 2 - }, - "mouseShadow" : { - "type":"bool", - "default" : true - }, - "cellBorders" : { - "type" : "bool", - "default" : false - }, - "stackRange" : { - "type" : "bool", - "default" : true - }, - "showQueue" : { - "type" : "bool", - "default" : true - } - }, - "default" : {} - }, - "server" : { - "type" : "object", - "properties" : { - "server" : { - "type":"string", - "default" : "127.0.0.1" - }, - "port" : { - "type" : "number", - "default" : 3030 - }, - "localInformation" : { - "type" : "number", - "default" : 2 - }, - "playerAI" : { - "type" : "string", - "default" : "VCAI" - }, - "neutralAI" : { - "type" : "string", - "default" : "StupidAI" - } - }, - "default" : {} - } -} diff --git a/config/factions/castle.json b/config/factions/castle.json index e2581fb99..2df9ee187 100644 --- a/config/factions/castle.json +++ b/config/factions/castle.json @@ -112,8 +112,7 @@ { "id" : 40, "animation" : "TBCSUP_3.def", "x" : 176, "y" : 85, "border" : "TOCSSWD2.bmp", "area" : "TZCSSWD2.bmp" }, { "id" : 41, "animation" : "TBCSUP_4.def", "x" : 563, "y" : 173, "z" : 1, "border" : "TOCSMON2.bmp", "area" : "TZCSMON2.bmp" }, { "id" : 42, "animation" : "TBCSUP_5.def", "x" : 160, "y" : 190, "z" : -1, "border" : "TOCSCAV2.bmp", "area" : "TZCSCAV2.bmp" }, - { "id" : 43, "animation" : "TBCSUP_6.def", "x" : 303, "y" : 0, "z" : -1, "border" : "TOCSANG2.bmp", "area" : "TZCSANG2.bmp" }, - + { "id" : 43, "animation" : "TBCSUP_6.def", "x" : 303, "y" : 0, "z" : -1, "border" : "TOCSANG2.bmp", "area" : "TZCSANG2.bmp" } ], "icons" : { diff --git a/config/schemas/artifact.json b/config/schemas/artifact.json new file mode 100644 index 000000000..687c99716 --- /dev/null +++ b/config/schemas/artifact.json @@ -0,0 +1,82 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI artifact format", + "description" : "Format used to define new artifacts in VCMI", + "required" : [ "class", "graphics", "text", "type", "value" ], + "properties":{ + "bonuses": { + "type":"array", + "description": "Bonuses provided by this artifact using bonus system", + "items": { "$ref" : "vcmi:bonus" } + }, + "class": { + "type":"string", + "enum" : [ "SPECIAL", "TREASURE", "MINOR", "MAJOR", "RELIC" ], + "description": "Artifact class, treasure, minor, major or relic" + }, + "components": { + "type":"array", + "description": "Optional, list of components for combinational artifacts", + "items": { "type":"string" } + }, + "graphics": { + "type":"object", + "description": "Graphical files associated with the artifact", + "required" : [ "iconIndex", "image", "map" ], + "properties":{ + "iconIndex": { + "type":"number", + "description": "index of icon of this artifact in .def file" + }, + "image": { + "type":"string", + "description": "Base image for this artifact, used for example in hero screen" + }, + "large": { + "type":"string", + "description": "Large image, used for drag-and-drop and popup messages" + }, + "map": { + "type":"string", + "description": ".def file for adventure map" + } + } + }, + "slot": { + "type":"string", + "description": "Slot to which this artifact can be put, if applicable" + }, + "text": { + "type":"object", + "description": "Texts associated with artifact", + "required" : [ "description", "event", "name" ], + "properties":{ + "description": { + "type":"string", + "description": "Long description of this artifact" + }, + "event": { + "type":"string", + "description": "Text that visible on picking this artifact on map" + }, + "name": { + "type":"string", + "description": "Name of the artifact" + } + } + }, + "type": { + "type":"array", + "description": "Type of this artifact - creature, hero or commander", + "items": { + "type":"string", + "enum" : [ "HERO", "CREATURE", "COMMANDER" ] + } + }, + "value": { + "type":"number", + "description": "Cost of this artifact, in gold" + } + } +} diff --git a/config/schemas/bonus.json b/config/schemas/bonus.json new file mode 100644 index 000000000..7b05d8362 --- /dev/null +++ b/config/schemas/bonus.json @@ -0,0 +1,92 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI bonus system format", + "description" : "Subsection of several formats, used to add generic bonuses to objects", + "required": ["type"], + "properties":{ + "addInfo": { + "anyOf" : [ + { "type" : "string" }, + { "type" : "number" } + ], + "description": "addInfo", + }, + "description": { + "type":"string", + "description": "description", + }, + "duration": { + "type":"string", + "description": "duration", + }, + "effectRange": { + "type":"string", + "description": "effectRange", + }, + "limiters": { + "type":"array", + "description": "limiters", + "items": { + "type":"object", + "properties" : { + "parameters": { + "type":"array", + "description" : "parameters", + "additionalItems": true + }, + "type": { + "type":"string", + "description": "type", + } + } + } + }, + "propagator": { + "description": "propagator", + "anyOf" : [ + { + "type" : "string" + }, + { + "type":"array", + "items": { + "type":"string", + "description": "0", + } + } + ], + }, + "sourceID": { + "type":"number", + "description": "sourceID", + }, + "sourceType": { + "type":"string", + "description": "sourceType", + }, + "subtype": { + "anyOf" : [ + { "type" : "string" }, + { "type" : "number" } + ], + "description": "subtype", + }, + "turns": { + "type":"number", + "description": "turns", + }, + "type": { + "type":"string", + "description": "type", + }, + "val": { + "type":"number", + "description": "val", + }, + "valueType": { + "type":"string", + "description": "valueType", + } + } +} diff --git a/config/schemas/creature.json b/config/schemas/creature.json new file mode 100644 index 000000000..98f90fd1f --- /dev/null +++ b/config/schemas/creature.json @@ -0,0 +1,244 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI creature format", + "description": "Json format for defining new creatures in VCMI", + "required" : [ + "name", "faction", "cost", "level", "fightValue", "aiValue", + "attack", "defense", "hitPoints", "speed", "damage", "advMapAmount", + "graphics", "sound" + ], + "properties":{ + "name": { + "type":"object", + "description": "Translatable names for this creature", + "required" : [ "singular", "plural" ], + "properties":{ + "singular": { + "type":"string", + "description": "Singular version" + }, + "plural": { + "type":"string", + "description": "Plural version" + } + } + }, + "faction": { + "type":"string", + "description": "Faction this creature belongs to. Examples: castle, rampart" + }, + "cost": { + "type":"object", + "description": "Cost to recruit this creature", + "properties":{ + "wood": { "type":"number"}, + "mercury": { "type":"number"}, + "ore": { "type":"number"}, + "sulfur": { "type":"number"}, + "crystal": { "type":"number"}, + "gems": { "type":"number"}, + "gold": { "type":"number"} + } + }, + "special": { + "type":"boolean", + "description": "Marks this object as special and not available by default" + }, + "level": { "type":"number"}, + "fightValue": { + "type":"number", + "description": " \"value\" of creature, used to determine for example army strength" + }, + "aiValue": { + "type":"number", + "description": "Describes how valuable this creature is to AI. Usually similar to fightValue" + }, + "growth": { + "type":"number", + "description": "Basic growth of this creature in town or in external dwellings" + }, + "horde": { + "type":"number", + "description": "Bonus growth of this creature from built horde" + }, + "attack": { "type":"number" }, + "defense": { "type":"number" }, + "hitPoints": { "type":"number" }, + "speed": { "type":"number" }, + "shots": { "type":"number" }, + + "damage": { + "type":"object", + "properties":{ + "max": { "type":"number" }, + "min": { "type":"number" } + } + }, + "spellPoints": { + "type":"number", + "description": "Spell points this creature has (usually equal to number of casts)" + }, + "advMapAmount": { + "type":"object", + "description" : "Initial size of random stacks on adventure map", + "properties":{ + "min": { "type":"number" }, + "max": { "type":"number" } + } + }, + "upgrades": { + "type":"array", + "description": "List of creatures to which this one can be upgraded", + "items": { "type":"string" } + }, + "doubleWide": { + "type":"boolean", + "description": "If set, creature will be two tiles wide on battlefield" + }, + "hasDoubleWeek": { + "type":"boolean", + "description": "creature may receive \"week of\" events" + }, + "abilities": { + "type":"array", + "description": "Creature abilities described using Bonus system", + "items": { "$ref" : "vcmi:bonus" } + }, + "stackExperience": { + "type":"array", + "description": "Stack experience, using bonus system", + "items":{ + "type":"object", + "required" : [ "bonus", "values" ], + "description": "0", + "properties":{ + "bonus": {"$ref" : "vcmi:bonus" }, + "values": { + "type":"array", + "minItems" : 10, + "maxItems" : 10, + "description": "Strength of the bonus", + "additionalItems" : true, + "anyof" : [ + { "items": { "type" : "number" } }, + { "items": { "type" : "boolean" } } + ] + } + } + } + }, + "graphics": { + "type":"object", + "description": "Describes how this creature looks like during battles", + "required" : [ + "animationTime", "iconLarge", "iconSmall", "iconIndex", + "map", "animation", "attackClimaxFrame", "timeBetweenFidgets" + ], + "properties":{ + "animationTime": { + "type":"object", + "required" : [ "attack", "flight", "walk" ], + "description": "Length of several animations", + "properties":{ + "attack": { + "type":"number", + "description": "attack" + }, + "flight": { + "type":"number", + "description": "flight" + }, + "walk": { + "type":"number", + "description": "walk" + } + } + }, + "iconLarge": { + "type":"string", + "description": "Large icon for this creature, used for example in town screen" + }, + "iconSmall": { + "type":"string", + "description": "Small icon for this creature, used for example in exchange screen" + }, + "iconIndex": { + "type":"number", + "description": "Index of icon of this creature in .def file" + }, + + "map": { + "type":"string", + "description": ".def file with animation of this creature on adventure map" + }, + "animation": { + "type":"string", + "description": ".def file with animation of this creature in battles" + }, + "attackClimaxFrame": { + "type":"number", + "description": "Frame from attack animation during which creature deals damage" + }, + "missile": { + "type":"object", + "required" : [ "projectile", "frameAngles", "offset" ], + "description": "Missile description for archers", + "properties":{ + "projectile": { + "type":"string", + "description": "Path to projectile animation" + }, + "frameAngles": { + "type":"array", + "description": "Angles of missile images, should go from 90 to -90", + "minItems" : 1, + "items": { "type":"number" } + }, + "offset": { + "type":"object", + "required" : [ "lowerX", "lowerY", "middleX", "middleY", "upperX", "upperY" ], + "description": "Position where projectile image appears during shooting in specific direction", + "properties":{ + "lowerX": { "type":"number" }, + "lowerY": { "type":"number" }, + "middleX": { "type":"number" }, + "middleY": { "type":"number" }, + "upperX": { "type":"number" }, + "upperY": { "type":"number" } + } + }, + "spinning": { + "type":"boolean", + "description": "Flying projectile spins. Has some requirements to .def file" + } + } + }, + "timeBetweenFidgets": { + "type":"number", + "description": "How often creature will play idling animation" + }, + "troopCountLocationOffset": { + "type":"number", + "description": "Position of troop count label?" + } + } + }, + "sound": { + "type":"object", + "description": "Various sound files associated with this creature", + "properties":{ + "attack": { "type":"string" }, + "defend": { "type":"string" }, + "killed": { "type":"string" }, + "moveEnd": { "type":"string" }, + "moveStart": { "type":"string" }, + "move": { "type":"string" }, + "shoot": { "type":"string" }, + "wince": { "type":"string" }, + "ext1": { "type":"string" }, + "ext2": { "type":"string" } + } + } + } +} diff --git a/config/schemas/faction.json b/config/schemas/faction.json new file mode 100644 index 000000000..113b2b805 --- /dev/null +++ b/config/schemas/faction.json @@ -0,0 +1,250 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI faction format", + "description": "Json format for defining new faction (aka towns) in VCMI", + "required" : [ "name", "alignment", "creatureBackground" ], + "dependencies" : { + "town" : [ "puzzleMap", "commander" ] + }, + "properties":{ + "name" : { + "type" : "string", + "description" : "Translatable name of town" + }, + "alignment": { + "type":"string", + "enum" : [ "good", "neutral", "evil" ], + "description": "Town alignment, good, neutral or evil" + }, + "commander": { + "type":"string", + "description": "Identifier of creature that is used as commander by heroes" + }, + "creatureBackground": { + "type":"object", + "required" : [ "120px", "130px" ], + "description": "Backgrounds for creature info card", + "properties":{ + "120px": { + "type":"string", + "description": "Version that is 120 pixels in height" + }, + "130px": { + "type":"string", + "description": "Version that is 130 pixels in height" + } + } + }, + "nativeTerrain": { + "type":"string", + "description": "Native terrain for creatures. Creatures fighting on native terrain receive several bonuses", + }, + "puzzleMap": { + "type":"object", + "required" : [ "prefix", "pieces" ], + "description": "Puzzle map from obelisks for this town. Must contain 48 pieces", + "properties":{ + "pieces": { + "type":"array", + "description": "Lits of pieces definitions", + "minItems" : 48, + "maxItems" : 48, + "items": { + "type":"object", + "properties":{ + "index": { "type":"number", "description" : "Order in which images will be opened" }, + "x": { "type":"number", "description" : "X coordinate on screen" }, + "y": { "type":"number", "description" : "X coordinate on screen" } + } + } + }, + "prefix": { + "type":"string", + "description": "Prefix for image names, e.g. \"PUZCAS\" for name \"PUZCAS12.png\"" + } + } + }, + "town": { + "type":"object", + "required" : [ + "adventureMap", "buildingsIcons", "buildings", "creatures", "guildWindow", "names", + "hallBackground", "hallSlots", "horde", "icons", "mageGuild", "moatDamage", + "musicTheme", "siege", "structures", "townBackground", "warMachine", "primaryResource" + ], + "description": "town", + "properties":{ + "adventureMap": { + "type":"object", + "description": "Paths to images of object on adventure map", + "required" : [ "capitol", "castle", "village" ], + "properties":{ + "capitol": { + "type":"string", + "description": "Town with capitol" + }, + "castle": { + "type":"string", + "description": "Town with built fort" + }, + "village": { + "type":"string", + "description": "Village without built fort" + }, + "dwellings" : { + "type" : "array", + "description" : "Dwellings on adventure map", + "items" : { + "type" : "object", + "properties" : { + "name": { "type":"string" }, + "graphics": { "type":"string" } + } + } + } + } + }, + "guildSpells" : { + "type" : "object", + "description" : "Spells available in mage guild. Value indicates chance for this spell to appear", + "additionalProperties" : { + "type" : "number" + } + }, + "tavern" : { + "type" : "object", + "description" : "Hero classes available in tavern. Value indicates chance for hero of this class to appear", + "additionalProperties" : { + "type" : "number" + } + }, + "names" : { + "type" : "array", + "description" : "Names for towns on adventure map", + "minItems" : 1, + "items" : { "type" : "string" } + }, + "buildingsIcons": { + "type" : "string", + "description": "Path to .def file with building icons" + }, + "buildings": { + "type" : "array", + "items" : { + "$ref" : "vcmi:townBuilding" + } + }, + "creatures": { + "type":"array", + "minItems" : 7, + "maxItems" : 7, + "description" : "List of creatures available for recruitment on each level", + "items": { + "type":"array", + "items" : { "type" : "string" } + } + }, + "guildWindow": { + "type":"string", + "description": "Image with small view on town from mage guild" + }, + "hallBackground": { + "type":"string", + "description": "background image for town hall" + }, + "hallSlots": { + "type":"array", + "description": "Description of town hall", + "minItems" : 5, + "maxItems" : 5, + "items": { + "type":"array", + "minItems" : 1, + "maxItems" : 4, + "items" : { + "type" : "array", + "description" : "List of buildings available in one slot", + "items" : { "type" : "number" } + } + } + }, + "horde": { + "type":"array", + "maxItems" : 2, + "description": "Levels of creatures that have hordes in town", + "items": { "type":"number" } + }, + "icons": { + "type":"object", + "description": "Town icons", + "required" : [ "fort", "village" ], + "properties":{ + "fort": { + "type":"object", + "required" : [ "normal", "built" ], + "description": "Icons for town with built fort", + "properties":{ + "built": { + "type":"number", + "description": "Icon used after player build something in town" + }, + "normal": { + "type":"number", + "description": "Icon used normally" + } + } + }, + "village": { + "type":"object", + "required" : [ "normal", "built" ], + "description": "Icons for town without fort", + "properties":{ + "built": { + "type":"number", + "description": "Icon used after player build something in town" + }, + "normal": { + "type":"number", + "description": "Icon used normally" + } + } + } + } + }, + "mageGuild": { + "type":"number", + "description": "Maximal level of mage guild" + }, + "moatDamage": { + "type":"number", + "description": "Damage dealt to creature that entered town moat during siege" + }, + "musicTheme": { + "type":"string", + "description": "Path to town music theme" + }, + "siege": { + "$ref" : "vcmi:townSiege" + }, + "structures": { + "type" : "array", + "items" : { + "$ref" : "vcmi:townStructure" + } + }, + "townBackground": { + "type":"string", + "description": "Background for town screen" + }, + "primaryResource": { + "type":"string", + "description": "Primary resource for this town. Produced by Silo and offered as starting bonus" + }, + "warMachine": { + "type":"string", + "description": "Identifier of war machine produced by blacksmith in town" + } + } + } + } +} diff --git a/config/schemas/hero.json b/config/schemas/hero.json new file mode 100644 index 000000000..e87607d4e --- /dev/null +++ b/config/schemas/hero.json @@ -0,0 +1,151 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI hero format", + "description" : "Format used to define new heroes in VCMI", + "required": [ "army", "class", "images", "skills", "texts" ], + "properties":{ + "army": { + "type":"array", + "description": "Initial hero army when recruited in tavern", + "minItems" : 1, + "maxItems" : 3, + "items": { + "type":"object", + "required" : [ "creature", "min", "max" ], + "properties":{ + "creature": { + "type":"string", + "description": "creature", + }, + "max": { + "type":"number", + "description": "max", + }, + "min": { + "type":"number", + "description": "min", + } + } + } + }, + "special": { + "type":"boolean", + "description": "Marks this object as special and not available by default" + }, + "class": { + "type":"string", + "description": "Hero class, e.g. knight or battleMage" + }, + "female": { + "type":"boolean", + "description": "This hero is female by default" + }, + "images": { + "type":"object", + "description": "images", + "required": [ "index", "large", "small", "specialtyLarge", "specialtySmall" ], + "properties":{ + "index": { + "type":"number", + "description": "Index of image in portraitsLarge/portraitsSmall files" + }, + "large": { + "type":"string", + "description": "Large version of portrait for use in hero screen" + }, + "small": { + "type":"string", + "description": "Small version of portrait for use on adventure map" + }, + "specialtyLarge": { + "type":"string", + "description": "Large image of hero specilty, used in hero screen" + }, + "specialtySmall": { + "type":"string", + "description": "Small image of hero specialty for use in exchange screen" + } + } + }, + "skills": { + "type":"array", + "description": "List of skills initially known by hero", + "maxItems" : 8, + "items": { + "type":"object", + "required" : [ "level", "skill" ], + "properties":{ + "level": { + "type":"string", + "description": "level", + "enum" : [ "basic", "advanced", "expert" ] + }, + "skill": { + "type":"string", + "description": "skill" + } + } + } + }, + "specialty": { + "type":"array", + "description": "Description of hero specialty using bonus system", + "items": { + "type":"object", + "required" : [ "bonuses" ], + "properties":{ + "growsWithLevel" : { + "type" : "boolean", + "description" : "Specialty growth with level, so far only SECONDARY_SKILL_PREMY and PRIMATY SKILL with creature limiter can grow" + }, + "bonuses": { + "type":"array", + "description": "List of bonuses", + "items": { "$ref" : "vcmi:bonus" } + } + } + } + }, + "spellbook": { + "type":"array", + "description": "List of starting spells, if available. This entry (even empty) will also grant spellbook", + "items": { "type":"string" } + }, + "texts": { + "type":"object", + "description": "All translatable texts related to hero", + "required" : [ "biography", "name", "specialty" ], + "properties":{ + "biography": { + "type":"string", + "description": "Hero biography" + }, + "name": { + "type":"string", + "description": "Hero name" + }, + "specialty": { + "type":"object", + "description": "Hero specialty information", + "required" : [ "description", "name", "tooltip" ], + "properties":{ + "description": { + "type":"string", + "description": "Description visible when hovering over specialty icon" + }, + "name": { + "type":"string", + "description": "Name of the specialty" + }, + "tooltip": { + "type":"string", + "description": "Tooltip visible on clicking icon." + } + } + } + } + } + } +} + diff --git a/config/schemas/heroClass.json b/config/schemas/heroClass.json new file mode 100644 index 000000000..202169cc4 --- /dev/null +++ b/config/schemas/heroClass.json @@ -0,0 +1,92 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI hero class format", + "description" : "Format used to define classes of heroes in VCMI", + "required" : [ + "animation", "faction", "highLevelChance", "lowLevelChance", + "name", "primarySkills", "secondarySkills", "tavern" + ], + "properties":{ + "animation": { + "type":"object", + "description": "Files related to hero animation", + "required": [ "battle", "map" ], + "properties":{ + "battle": { + "type":"object", + "description": "Hero animations for battle", + "required": [ "female", "male" ], + "properties":{ + "female": { + "type":"string", + "description": "Female version" + }, + "male": { + "type":"string", + "description": "Male version" + } + } + }, + "map": { + "type":"object", + "description": "Hero animations for adventure map", + "required": [ "female", "male" ], + "properties":{ + "female": { + "type":"string", + "description": "Female version. Warning: not implemented!" + }, + "male": { + "type":"string", + "description": "Male version" + } + } + } + } + }, + "faction": { + "type":"string", + "description": "Faction this hero class belongs to" + }, + "highLevelChance": { + "type":"object", + "description": "Chance to get specific primary skill on level-up, applicable for levels starting from 10", + "additionalProperties":{ + "type":"number" + } + }, + "lowLevelChance": { + "type":"object", + "description": "Chance to get specific primary skill on level-up, applicable for levels less than 10", + "additionalProperties":{ + "type":"number" + } + }, + "name": { + "type":"string", + "description": "Translatable name of hero class" + }, + "primarySkills": { + "type":"object", + "description": "Initial primary skills of heroes", + "additionalProperties":{ + "type":"number" + } + }, + "secondarySkills": { + "type":"object", + "description": "Chance to get specific secondary skill on level-up. All missing skills are considered to be banned", + "additionalProperties":{ + "type":"number" + } + }, + "tavern": { + "type":"object", + "description": "Chance for this hero to appear in tavern of this factions. Reversed version of field \"tavern\" from town format", + "additionalProperties":{ + "type":"number" + } + } + } +} diff --git a/config/schemas/mod.json b/config/schemas/mod.json new file mode 100644 index 000000000..c7758cd49 --- /dev/null +++ b/config/schemas/mod.json @@ -0,0 +1,78 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI mod file format", + "description" : "Format used to define main mod file (mod.json) in VCMI", + "required" : [ "name", "description" ], + "properties":{ + + "name": { + "type":"string", + "description": "Short name of your mod. No more than 2-3 words" + }, + "description": { + "type":"string", + "description": "More lengthy description of mod. No hard limit" + }, + + "depends": { + "type":"array", + "description": "List of mods that are required to run this one", + "items": { "type":"string" } + }, + "conflicts": { + "type":"array", + "description": "List of mods that can't be enabled in the same time as this one", + "items": { "type":"string" } + }, + + "artifacts": { + "type":"array", + "description": "List of configuration files for artifacts", + "items": { "type":"string" } + }, + "creatures": { + "type":"array", + "description": "List of configuration files for creatures", + "items": { "type":"string" } + }, + "factions": { + "type":"array", + "description": "List of configuration files for towns/factions", + "items": { "type":"string" } + }, + "heroClasses": { + "type":"array", + "description": "List of configuration files for hero classes", + "items": { "type":"string" } + }, + "heroes": { + "type":"array", + "description": "List of configuration files for heroes", + "items": { "type":"string" } + }, + + "filesystem": { + "type":"object", + "description": "Optional, description on how files are organized in your mod. In most cases you do not need to use this field", + "additionalProperties":{ + "type":"array", + "description" : "list of data sources attached to this mount point", + "items": { + "type":"object", + "properties":{ + "path": { + "type":"string", + "description": "Path to data source" + }, + "type": { + "type" : "string", + "enum" : [ "dir", "lod", "snd", "vid" ], + "description" : "Type of data source" + } + } + } + } + } + } +} diff --git a/config/schemas/settings.json b/config/schemas/settings.json new file mode 100644 index 000000000..fce13a61b --- /dev/null +++ b/config/schemas/settings.json @@ -0,0 +1,133 @@ +// This is schema for checking game settings from settings.json +// Any new settings should be added in this file for correct serialization and initialization +{ + "type" : "object", + "$schema": "http://json-schema.org/draft-04/schema", + "required" : [ "general", "video", "adventure", "battle", "server" ], + "properties": + { + "general" : { + "type" : "object", + "required" : [ "classicCreatureWindow", "playerName", "showfps", "music", "sound" ], + "properties" : { + "classicCreatureWindow" : { + "type" : "boolean", + "default" : false + }, + "playerName" : { + "type":"string", + "default" : "Player" + }, + "showfps" : { + "type" : "boolean", + "default" : false + }, + "music" : { + "type" : "number", + "default" : 88 + }, + "sound" : { + "type" : "number", + "default" : 88 + } + } + }, + "video" : { + "type" : "object", + "required" : [ "screenRes", "bitsPerPixel", "fullscreen" ], + "properties" : { + "screenRes" : { + "type" : "object", + "required" : [ "width", "height" ], + "properties" : { + "width" : { "type" : "number" }, + "height" : { "type" : "number" } + }, + "default": {"width" : 800, "height": 600 } + }, + "bitsPerPixel" : { + "type" : "number", + "default" : 24 + }, + "fullscreen" : { + "type" : "boolean", + "default" : false + } + } + }, + "adventure" : { + "type" : "object", + "required" : [ "heroSpeed", "enemySpeed", "scrollSpeed", "heroReminder" ], + "properties" : { + "heroSpeed" : { + "type" : "number", + "default" : 2 + }, + "enemySpeed" : { + "type" : "number", + "default" : 2 + }, + "scrollSpeed" : { + "type" : "number", + "default" : 1 + }, + "heroReminder" : { + "type" : "boolean", + "default" : true + } + } + }, + "battle" : { + "type" : "object", + "required" : [ "animationSpeed", "mouseShadow", "cellBorders", "stackRange", "showQueue" ], + "properties" : { + "animationSpeed" : { + "type" : "number", + "default" : 2 + }, + "mouseShadow" : { + "type":"boolean", + "default" : true + }, + "cellBorders" : { + "type" : "boolean", + "default" : false + }, + "stackRange" : { + "type" : "boolean", + "default" : true + }, + "showQueue" : { + "type" : "boolean", + "default" : true + } + } + }, + "server" : { + "type" : "object", + "required" : [ "server", "port", "localInformation", "playerAI", "neutralAI" ], + "properties" : { + "server" : { + "type":"string", + "default" : "127.0.0.1" + }, + "port" : { + "type" : "number", + "default" : 3030 + }, + "localInformation" : { + "type" : "number", + "default" : 2 + }, + "playerAI" : { + "type" : "string", + "default" : "VCAI" + }, + "neutralAI" : { + "type" : "string", + "default" : "StupidAI" + } + } + } + } +} diff --git a/config/schemas/townBuilding.json b/config/schemas/townBuilding.json new file mode 100644 index 000000000..99bf622a1 --- /dev/null +++ b/config/schemas/townBuilding.json @@ -0,0 +1,50 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI town building format", + "description" : "Format used to define town buildings in VCMI", + "required": [ "id" ], + "properties":{ + "id": { + "type":"number", + "description" : "Numeric identifier of this building" + }, + "mode": { + "type":"string", + "enum" : [ "normal", "auto", "special", "grail" ], + "description" : "Mode in which this building will be built" + }, + "requires": { + "type":"array", + "description" : "List of town buildings that must be built before this one", + "items": { + "type":"number" + } + }, + "upgrades": { + "description" : "If this building is upgrade, identifier of base building", + "type":"number" + }, + "name" : { + "description" : "Name of this building", + "type" : "string" + }, + "description" : { + "description" : "Full decsription of this building", + "type" : "string" + }, + "cost": { + "type":"object", + "description": "Cost to build this building", + "properties":{ + "wood": { "type":"number"}, + "mercury": { "type":"number"}, + "ore": { "type":"number"}, + "sulfur": { "type":"number"}, + "crystal": { "type":"number"}, + "gems": { "type":"number"}, + "gold": { "type":"number"} + } + } + } +} diff --git a/config/schemas/townSiege.json b/config/schemas/townSiege.json new file mode 100644 index 000000000..5f4453c9c --- /dev/null +++ b/config/schemas/townSiege.json @@ -0,0 +1,349 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI town building format", + "description" : "Format used to define town buildings in VCMI", + "id": "#", + "properties":{ + "gate": { + "type":"object", + "id": "gate", + "properties":{ + "arch": { + "type":"object", + "id": "arch", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "gate": { + "type":"object", + "id": "gate", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + } + } + }, + "imagePrefix": { + "type":"string", + "id": "imagePrefix" + }, + "moat": { + "type":"object", + "id": "moat", + "properties":{ + "bank": { + "type":"object", + "id": "bank", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "moat": { + "type":"object", + "id": "moat", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + } + } + }, + "shooterHeight": { + "type":"number", + "id": "shooterHeight" + }, + "shooter": { + "type":"string", + "id": "shooter" + }, + "static": { + "type":"object", + "id": "static", + "properties":{ + "background": { + "type":"object", + "id": "background", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "bottom": { + "type":"object", + "id": "bottom", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "top": { + "type":"object", + "id": "top", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + } + } + }, + "towers": { + "type":"object", + "id": "towers", + "properties":{ + "bottom": { + "type":"object", + "id": "bottom", + "properties":{ + "battlement": { + "type":"object", + "id": "battlement", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "creature": { + "type":"object", + "id": "creature", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "tower": { + "type":"object", + "id": "tower", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + } + } + }, + "keep": { + "type":"object", + "id": "keep", + "properties":{ + "battlement": { + "type":"object", + "id": "battlement", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "creature": { + "type":"object", + "id": "creature", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "tower": { + "type":"object", + "id": "tower", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + } + } + }, + "top": { + "type":"object", + "id": "top", + "properties":{ + "battlement": { + "type":"object", + "id": "battlement", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "creature": { + "type":"object", + "id": "creature", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "tower": { + "type":"object", + "id": "tower", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + } + } + } + } + }, + "walls": { + "type":"object", + "id": "walls", + "properties":{ + "bottomMid": { + "type":"object", + "id": "bottomMid", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "bottom": { + "type":"object", + "id": "bottom", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "upperMid": { + "type":"object", + "id": "upperMid", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + }, + "upper": { + "type":"object", + "id": "upper", + "properties":{ + "x": { + "type":"number", + "id": "x" + }, + "y": { + "type":"number", + "id": "y" + } + } + } + } + } + } +} diff --git a/config/schemas/townStructure.json b/config/schemas/townStructure.json new file mode 100644 index 000000000..eedd9970e --- /dev/null +++ b/config/schemas/townStructure.json @@ -0,0 +1,45 @@ +{ + "type":"object", + "$schema": "http://json-schema.org/draft-04/schema", + "title" : "VCMI town structures format", + "description" : "Format used to define structures visible on town screen in VCMI", + "required": [ "animation", "x", "y"], + "properties":{ + "animation": { + "type":"string", + "description" : "Main animation file for this building" + }, + "area": { + "type":"string", + "description" : "Area that indicate when building is selected. Must be 8-bit image" + }, + "border": { + "type":"string", + "description" : "Golden border around building, displayed when building is selected" + }, + "builds": { + "type":"number", + "description" : "" + }, + "hidden": { + "type":"boolean", + "description" : "If upgrade, this building will replace parent animation but will not alter its behaviour" + }, + "id": { + "type":"number", + "description" : "Numeric identifier of building" + }, + "x": { + "type":"number", + "description" : "Position on screen" + }, + "y": { + "type":"number", + "description" : "Position on screen" + }, + "z": { + "type":"number", + "description" : "Position on screen. Buildings with higher value will be drawn on top of other buildings" + } + } +} diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 7b4138971..913336f14 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -250,7 +250,9 @@ CArtifact * CArtHandler::loadArtifact(const JsonNode & node) art->iconIndex = graphics["iconIndex"].Float(); art->image = graphics["image"].String(); - if (!graphics["large"].loadTo(art->large)) + if (!graphics["large"].isNull()) + art->large = graphics["large"].String(); + else art->large = art->image; art->advMapDef = graphics["map"].String(); diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index 263631e10..04586ac4f 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -59,8 +59,9 @@ void SettingsStorage::init() { CResourceHandler::get()->createResource("config/settings.json"); JsonNode(ResourceID("config/settings.json")).swap(config); - JsonNode schema(ResourceID("config/defaultSettings.json")); - JsonUtils::validate(config, schema); + + JsonUtils::maximize(config, "vcmi:settings"); + JsonUtils::validate(config, "vcmi:settings", "settings"); } void SettingsStorage::invalidateNode(const std::vector &changedPath) @@ -69,10 +70,10 @@ void SettingsStorage::invalidateNode(const std::vector &changedPath listener->nodeInvalidated(changedPath); JsonNode savedConf = config; - JsonNode schema(ResourceID("config/defaultSettings.json")); + JsonNode schema(ResourceID("config/schemas/settings.json")); savedConf.Struct().erase("session"); - JsonUtils::minimize(savedConf, schema); + JsonUtils::minimize(savedConf, "vcmi:settings"); std::ofstream file(CResourceHandler::get()->getResourceName(ResourceID("config/settings.json")), std::ofstream::trunc); file << savedConf; diff --git a/lib/CConsoleHandler.cpp b/lib/CConsoleHandler.cpp index eb32ce9ba..8040893e1 100644 --- a/lib/CConsoleHandler.cpp +++ b/lib/CConsoleHandler.cpp @@ -16,10 +16,10 @@ #ifndef _WIN32 typedef std::string TColor; #define CONSOLE_GREEN "\x1b[1;32m" - #define CONSOLE_RED "\x1b[1;32m" + #define CONSOLE_RED "\x1b[1;31m" #define CONSOLE_MAGENTA "\x1b[1;35m" - #define CONSOLE_YELLOW "\x1b[1;32m" - #define CONSOLE_WHITE "\x1b[1;39m" + #define CONSOLE_YELLOW "\x1b[1;33m" + #define CONSOLE_WHITE "\x1b[1;37m" #define CONSOLE_GRAY "\x1b[1;30m" #define CONSOLE_TEAL "\x1b[1;36m" #else diff --git a/lib/CModHandler.cpp b/lib/CModHandler.cpp index ead464866..998cf0a42 100644 --- a/lib/CModHandler.cpp +++ b/lib/CModHandler.cpp @@ -25,7 +25,7 @@ void CIdentifierStorage::checkIdentifier(std::string & ID) { if (boost::algorithm::ends_with(ID, ".")) - tlog0 << "BIG WARNING: identifier " << ID << " seems to be broken!\n"; + tlog3 << "BIG WARNING: identifier " << ID << " seems to be broken!\n"; else { size_t pos = 0; @@ -33,7 +33,7 @@ void CIdentifierStorage::checkIdentifier(std::string & ID) { if (std::tolower(ID[pos]) != ID[pos] ) //Not in camelCase { - tlog0 << "Warning: identifier " << ID << " is not in camelCase!\n"; + tlog3 << "Warning: identifier " << ID << " is not in camelCase!\n"; ID[pos] = std::tolower(ID[pos]);// Try to fix the ID } pos = ID.find('.', pos); @@ -53,7 +53,7 @@ void CIdentifierStorage::requestIdentifier(std::string name, const boost::functi else { if(boost::algorithm::starts_with(name, "primSkill.")) - tlog2 << "incorrect primSkill name requested\n"; + tlog3 << "incorrect primSkill name requested\n"; missingObjects[name].push_back(callback); // queue callback } @@ -168,7 +168,7 @@ bool CModHandler::checkDependencies(const std::vector & input) const { if (!vstd::contains(input, dep)) { - tlog0 << "Error: Mod " << mod.name << " requires missing " << dep << "!\n"; + tlog1 << "Error: Mod " << mod.name << " requires missing " << dep << "!\n"; return false; } } @@ -177,7 +177,7 @@ bool CModHandler::checkDependencies(const std::vector & input) const { if (vstd::contains(input, conflicting)) { - tlog0 << "Error: Mod " << mod.name << " conflicts with " << allMods.at(conflicting).name << "!\n"; + tlog1 << "Error: Mod " << mod.name << " conflicts with " << allMods.at(conflicting).name << "!\n"; return false; } } @@ -269,7 +269,7 @@ void CModHandler::initialize(std::vector availableMods) if (!checkDependencies(detectedMods)) { - tlog0 << "Critical error: failed to load mods! Exiting...\n"; + tlog1 << "Critical error: failed to load mods! Exiting...\n"; exit(1); } @@ -282,21 +282,21 @@ void CModHandler::initialize(std::vector availableMods) file << modConfig; } - std::vector CModHandler::getActiveMods() { return activeMods; } template -void handleData(Handler handler, const JsonNode & sourceList) +void CModHandler::handleData(Handler handler, const JsonNode & source, std::string listName, std::string schemaName) { - JsonNode config = JsonUtils::assembleFromFiles(sourceList.convertTo >()); + JsonNode config = JsonUtils::assembleFromFiles(source[listName].convertTo >()); BOOST_FOREACH(auto & entry, config.Struct()) { if (!entry.second.isNull()) // may happens if mod removed object by setting json entry to null { + JsonUtils::validate(entry.second, schemaName, entry.first); handler->load(entry.first, entry.second); } } @@ -306,20 +306,21 @@ void CModHandler::loadActiveMods() { BOOST_FOREACH(const TModID & modName, activeMods) { - tlog1 << "\t\tLoading mod "; + tlog0 << "\t\tLoading mod "; tlog2 << allMods[modName].name << "\n"; std::string modFileName = "mods/" + modName + "/mod.json"; const JsonNode config = JsonNode(ResourceID(modFileName)); + JsonUtils::validate(config, "vcmi:mod", modName); - handleData(VLC->townh, config["factions"]); - handleData(VLC->creh, config["creatures"]); - handleData(VLC->arth, config["artifacts"]); + handleData(VLC->townh, config, "factions", "vcmi:faction"); + handleData(VLC->creh, config, "creatures", "vcmi:creature"); + handleData(VLC->arth, config, "artifacts", "vcmi:artifact"); //todo: spells - handleData(&VLC->heroh->classes, config["heroClasses"]); - handleData(VLC->heroh, config["heroes"]); + handleData(&VLC->heroh->classes, config,"heroClasses", "vcmi:heroClass"); + handleData(VLC->heroh, config, "heroes", "vcmi:hero"); } VLC->creh->buildBonusTreeForTiers(); //do that after all new creatures are loaded diff --git a/lib/CModHandler.h b/lib/CModHandler.h index e758e612b..ccc0a3456 100644 --- a/lib/CModHandler.h +++ b/lib/CModHandler.h @@ -75,7 +75,7 @@ class DLL_LINKAGE CModHandler bool hasCircularDependency(TModID mod, std::set currentList = std::set ()) const; - //returns false if mod list is incorrec and prints error to console. Possible errors are: + //returns false if mod list is incorrect and prints error to console. Possible errors are: // - missing dependency mod // - conflicting mod in load order // - circular dependencies @@ -84,7 +84,12 @@ class DLL_LINKAGE CModHandler // returns load order in which all dependencies are resolved, e.g. loaded after required mods // function assumes that input list is valid (checkDependencies returned true) std::vector resolveDependencies(std::vector input) const; + + // helper for loadActiveMods. Loads content from list of files + template + void handleData(Handler handler, const JsonNode & source, std::string listName, std::string schemaName); public: + CIdentifierStorage identifiers; /// receives list of available mods and trying to load mod.json from all of them diff --git a/lib/CObjectHandler.cpp b/lib/CObjectHandler.cpp index 36cbeead9..d72d046a9 100644 --- a/lib/CObjectHandler.cpp +++ b/lib/CObjectHandler.cpp @@ -2299,6 +2299,8 @@ int CGTownInstance::getBoatType() const case EAlignment::GOOD : return 1; case EAlignment::NEUTRAL : return 2; } + assert(0); + return -1; } int CGTownInstance::getMarketEfficiency() const diff --git a/lib/Filesystem/CResourceLoader.cpp b/lib/Filesystem/CResourceLoader.cpp index 15bc805ce..0decd4621 100644 --- a/lib/Filesystem/CResourceLoader.cpp +++ b/lib/Filesystem/CResourceLoader.cpp @@ -420,7 +420,8 @@ std::vector CResourceHandler::getAvailableMods() name.erase(0, name.find_last_of('/') + 1); //Remove path prefix - foundMods.push_back(name); + if (!name.empty()) // this is also triggered for "ALL/MODS/" entry + foundMods.push_back(name); ++iterator; } return foundMods; diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 04469ed8d..9940f3835 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -11,6 +11,8 @@ #include "StdInc.h" #include "JsonNode.h" +#include "ScopeGuard.h" + #include "HeroBonus.h" #include "Filesystem/CResourceLoader.h" #include "VCMI_Lib.h" //for identifier resolution @@ -34,8 +36,6 @@ JsonNode::JsonNode(const char *data, size_t datasize): { JsonParser parser(data, datasize); *this = parser.parse(""); - - JsonValidator validator(*this); } JsonNode::JsonNode(ResourceID && fileURI): @@ -45,8 +45,6 @@ JsonNode::JsonNode(ResourceID && fileURI): JsonParser parser(reinterpret_cast(file.first.get()), file.second); *this = parser.parse(fileURI.getName()); - - JsonValidator validator(*this); } JsonNode::JsonNode(const JsonNode ©): @@ -144,6 +142,11 @@ bool JsonNode::isNull() const return type == DATA_NULL; } +void JsonNode::clear() +{ + setType(DATA_NULL); +} + bool & JsonNode::Bool() { setType(DATA_BOOL); @@ -231,6 +234,46 @@ const JsonNode & JsonNode::operator[](std::string child) const return it->second; return nullNode; } + +// to avoid duplicating const and non-const code +template +Node & resolvePointer(Node & in, const std::string & pointer) +{ + if (pointer.empty()) + return in; + assert(pointer[0] == '/'); + + size_t splitPos = pointer.find('/', 1); + + std::string entry = pointer.substr(1, splitPos -1); + std::string remainer = splitPos == std::string::npos ? "" : pointer.substr(splitPos); + + if (in.getType() == JsonNode::DATA_VECTOR) + { + if (entry.find_first_not_of("0123456789") != std::string::npos) // non-numbers in string + throw std::runtime_error("Invalid Json pointer"); + + if (entry.size() > 1 && entry[0] == '0') // leading zeros are not allowed + throw std::runtime_error("Invalid Json pointer"); + + size_t index = boost::lexical_cast(entry); + + if (in.Vector().size() > index) + return in.Vector()[index].resolvePointer(remainer); + } + return in[entry].resolvePointer(remainer); +} + +const JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) const +{ + return ::resolvePointer(*this, jsonPointer); +} + +JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) +{ + return ::resolvePointer(*this, jsonPointer); +} + //////////////////////////////////////////////////////////////////////////////// template @@ -516,7 +559,7 @@ bool JsonParser::extractNull(JsonNode &node) if (!extractLiteral("null")) return false; - node.setType(JsonNode::DATA_NULL); + node.clear(); return true; } @@ -696,166 +739,370 @@ bool JsonParser::error(const std::string &message, bool warning) static const std::map stringToType = boost::assign::map_list_of - ("null", JsonNode::DATA_NULL) ("bool", JsonNode::DATA_BOOL) - ("number", JsonNode::DATA_FLOAT) ("string", JsonNode::DATA_STRING) - ("array", JsonNode::DATA_VECTOR) ("object", JsonNode::DATA_STRUCT); + ("null", JsonNode::DATA_NULL) ("boolean", JsonNode::DATA_BOOL) + ("number", JsonNode::DATA_FLOAT) ("string", JsonNode::DATA_STRING) + ("array", JsonNode::DATA_VECTOR) ("object", JsonNode::DATA_STRUCT); -//Check current schema entry for validness and converts "type" string to JsonType -bool JsonValidator::validateSchema(JsonNode::JsonType &type, const JsonNode &schema) +std::string JsonValidator::validateEnum(const JsonNode &node, const JsonVector &enumeration) { - if (schema.isNull()) - return addMessage("Missing schema for current entry!"); - - const JsonNode &nodeType = schema["type"]; - if (nodeType.isNull()) - return addMessage("Entry type is not defined in schema!"); - - if (nodeType.getType() != JsonNode::DATA_STRING) - return addMessage("Entry type must be string!"); - - std::map::const_iterator iter = stringToType.find(nodeType.String()); - - if (iter == stringToType.end()) - return addMessage("Unknown entry type found!"); - - type = iter->second; - return true; + BOOST_FOREACH(auto & enumEntry, enumeration) + { + if (node == enumEntry) + return ""; + } + return fail("Key must have one of predefined values"); } -//Replaces node with default value if needed and calls type-specific validators -bool JsonValidator::validateType(JsonNode &node, const JsonNode &schema, JsonNode::JsonType type) +std::string JsonValidator::validatesSchemaList(const JsonNode &node, const JsonNode &schemas, std::string errorMsg, std::function isValid) { - if (node.isNull()) + std::string errors; + if (!schemas.isNull()) { - const JsonNode & defaultValue = schema["default"]; - if (defaultValue.isNull()) - return addMessage("Null entry without default entry!"); - else - node = defaultValue; + size_t result = 0; + + BOOST_FOREACH(auto & schema, schemas.Vector()) + { + std::string error = validateNode(node, schema); + if (error.empty()) + result++; + else + errors += fail(error); + } + if (isValid(result)) + { + return ""; + } + return fail(errorMsg) + errors; } - if (minimize && node == schema["default"]) + return ""; +} + +std::string JsonValidator::validateNodeType(const JsonNode &node, const JsonNode &schema) +{ + std::string errors; + + // data must be valid against all schemas in the list + errors += validatesSchemaList(node, schema["allOf"], "Failed to pass all schemas", [&](size_t count) { - node.setType(JsonNode::DATA_NULL); - return false; - } + return count == schema["allOf"].Vector().size(); + }); - if (type != node.getType()) + // data must be valid against any non-zero number of schemas in the list + errors += validatesSchemaList(node, schema["anyOf"], "Failed to pass any schema", [&](size_t count) { - node.setType(JsonNode::DATA_NULL); - return addMessage("Type mismatch!"); + return count > 0; + }); + + // data must be valid against one and only one schema + errors += validatesSchemaList(node, schema["oneOf"], "Failed to pass only one and only one schema", [&](size_t count) + { + return count == 1; + }); + + // data must NOT be valid against schema + if (!schema["not"].isNull()) + { + if (validateNode(node, schema["not"]).empty()) + errors += fail("Successful validation against negative check"); } - - if (type == JsonNode::DATA_VECTOR) - return validateItems(node, schema["items"]); - - if (type == JsonNode::DATA_STRUCT) - return validateProperties(node, schema["properties"]); - - return true; + // basic schema check + if (!schema["type"].isNull()) + { + JsonNode::JsonType type = stringToType.find(schema["type"].String())->second; + if(type != node.getType()) + errors += fail("Type mismatch!"); + } + return errors; } // Basic checks common for any nodes -bool JsonValidator::validateNode(JsonNode &node, const JsonNode &schema, const std::string &name) +std::string JsonValidator::validateNode(const JsonNode &node, const JsonNode &schema) { - currentPath.push_back(name); + std::string errors; - JsonNode::JsonType type = JsonNode::DATA_NULL; - if (!validateSchema(type, schema) - || !validateType(node, schema, type)) + assert(!schema.isNull()); // can this error be triggered? + + if (!schema["$ref"].isNull()) + { + std::string URI = schema["$ref"].String(); + //node must be validated using schema pointed by this reference and not by data here + //Local reference. Turn it into more easy to handle remote ref + if (boost::algorithm::starts_with(URI, "#")) + URI = usedSchemas.back() + URI; + + return validateRoot(node, URI); + } + + errors += validateNodeType(node, schema); + + // enumeration - data must be equeal to one of items in list + if (!schema["enum"].isNull()) + errors += validateEnum(node, schema["enum"].Vector()); + + // try to run any type-specific checks + if (node.getType() == JsonNode::DATA_VECTOR) errors += validateVector(node, schema); + if (node.getType() == JsonNode::DATA_STRUCT) errors += validateStruct(node, schema); + if (node.getType() == JsonNode::DATA_STRING) errors += validateString(node, schema); + if (node.getType() == JsonNode::DATA_FLOAT) errors += validateNumber(node, schema); + + return errors; +} + +std::string JsonValidator::validateVectorItem(const JsonVector items, const JsonNode & schema, const JsonNode & additional, size_t index) +{ + currentPath.push_back(JsonNode()); + currentPath.back().Float() = index; + auto onExit = vstd::makeScopeGuard([&] { - node.setType(JsonNode::DATA_NULL); currentPath.pop_back(); - return false; - } - currentPath.pop_back(); - return true; -} - -//Checks "items" entry from schema (type-specific check for Vector) -bool JsonValidator::validateItems(JsonNode &node, const JsonNode &schema) -{ - JsonNode::JsonType type = JsonNode::DATA_NULL; - if (!validateSchema(type, schema)) - return false; - - bool result = true; - BOOST_FOREACH(JsonNode &entry, node.Vector()) - { - if (!validateType(entry, schema, type)) - { - result = false; - entry.setType(JsonNode::DATA_NULL); - } - } - return result; -} - -//Checks "propertries" entry from schema (type-specific check for Struct) -bool JsonValidator::validateProperties(JsonNode &node, const JsonNode &schema) -{ - if (schema.isNull()) - return addMessage("Properties entry is missing for struct in schema"); - - BOOST_FOREACH(auto & schemaEntry, schema.Struct()) - { - if (!validateNode(node[schemaEntry.first], schemaEntry.second, schemaEntry.first)) - node.Struct().erase(schemaEntry.first); - } - - for (auto iter = node.Struct().begin(); iter!= node.Struct().end();) - { - if (!vstd::contains(schema.Struct(), iter->first)) - { - addMessage("Missing schema for entry " + iter->first + "!"); - iter = node.Struct().erase(iter); - } - else - iter++; - } - return true; -} - -bool JsonValidator::addMessage(const std::string &message) -{ - std::ostringstream stream; - - stream << "At "; - BOOST_FOREACH(const std::string &path, currentPath) - stream << path<<"/"; - stream << "\t Error: " << message <<"\n"; - errors += stream.str(); - return false; -} - -JsonValidator::JsonValidator(JsonNode &root, bool Minimize): - minimize(Minimize) -{ - if (root.getType() != JsonNode::DATA_STRUCT) - return; - - JsonNode schema; - schema.swap(root["schema"]); - root.Struct().erase("schema"); + }); if (!schema.isNull()) { - validateProperties(root, schema); + // case 1: schema is vector. Validate items agaist corresponding items in vector + if (schema.getType() == JsonNode::DATA_VECTOR) + { + if (schema.Vector().size() > index) + return validateNode(items[index], schema.Vector()[index]); + } + else // case 2: schema has to be struct. Apply it to all items, completely ignore additionalItems + { + return validateNode(items[index], schema); + } } - //This message is quite annoying now - most files do not have schemas. May be re-enabled later - //else - // addMessage("Schema not found!", true); - //TODO: better way to show errors (like printing file name as well) - tlog3< schema["maxItems"].Float()) + errors += fail("Too many items in the list!"); + + if (vstd::contains(schema.Struct(), "minItems") && vector.size() < schema["minItems"].Float()) + errors += fail("Too few items in the list"); + + if (schema["uniqueItems"].Bool()) + { + for (auto itA = vector.begin(); itA != vector.end(); itA++) + { + auto itB = itA; + while (++itB != vector.end()) + { + if (*itA == *itB) + errors += fail("List must consist from unique items"); + } + } + } + return errors; +} + +std::string JsonValidator::validateStructItem(const JsonNode &node, const JsonNode & schema, const JsonNode & additional, std::string nodeName) +{ + currentPath.push_back(JsonNode()); + currentPath.back().String() = nodeName; + auto onExit = vstd::makeScopeGuard([&] + { + currentPath.pop_back(); + }); + + // there is schema specifically for this item + if (!schema[nodeName].isNull()) + return validateNode(node, schema[nodeName]); + + // try generic additionalItems schema + if (additional.getType() == JsonNode::DATA_STRUCT) + return validateNode(node, additional); + + if (!additional.Bool()) + return fail("Unknown entry found: " + nodeName); + + return ""; +} + +//Checks "properties" entry from schema (type-specific check for Struct) +std::string JsonValidator::validateStruct(const JsonNode &node, const JsonNode &schema) +{ + std::string errors; + auto & map = node.Struct(); + + BOOST_FOREACH(auto & entry, map) + errors += validateStructItem(entry.second, schema["properties"], schema["additionalProperties"], entry.first); + + BOOST_FOREACH(auto & required, schema["required"].Vector()) + { + if (!vstd::contains(map, required.String())) + errors += fail("Required entry " + required.String() + " is missing"); + } + + //Copy-paste from vector code. yay! + if (vstd::contains(schema.Struct(), "maxProperties") && map.size() > schema["maxProperties"].Float()) + errors += fail("Too many items in the list!"); + + if (vstd::contains(schema.Struct(), "minItems") && map.size() < schema["minItems"].Float()) + errors += fail("Too few items in the list"); + + if (schema["uniqueItems"].Bool()) + { + for (auto itA = map.begin(); itA != map.end(); itA++) + { + auto itB = itA; + while (++itB != map.end()) + { + if (itA->second == itB->second) + errors += fail("List must consist from unique items"); + } + } + } + + // dependencies. Format is object/struct where key is the name of key in data + // and value is either: + // a) array of fields that must be present + // b) struct with schema against which data should be valid + // These checks are triggered only if key is present + BOOST_FOREACH(auto & deps, schema["dependencies"].Struct()) + { + if (vstd::contains(map, deps.first)) + { + if (deps.second.getType() == JsonNode::DATA_VECTOR) + { + JsonVector depList = deps.second.Vector(); + BOOST_FOREACH(auto & depEntry, depList) + { + if (!vstd::contains(map, depEntry.String())) + errors += fail("Property " + depEntry.String() + " required for " + deps.first + " is missing"); + } + } + else + { + if (!validateNode(node, deps.second).empty()) + errors += fail("Requirements for " + deps.first + " are not fulfilled"); + } + } + } + + // TODO: missing fields from draft v4 + // patternProperties + return errors; +} + +std::string JsonValidator::validateString(const JsonNode &node, const JsonNode &schema) +{ + std::string errors; + auto & string = node.String(); + + if (vstd::contains(schema.Struct(), "maxLength") && string.size() > schema["maxLength"].Float()) + errors += fail("String too long"); + + if (vstd::contains(schema.Struct(), "minLength") && string.size() < schema["minLength"].Float()) + errors += fail("String too short"); + + // TODO: missing fields from draft v4 + // pattern + return errors; +} + +std::string JsonValidator::validateNumber(const JsonNode &node, const JsonNode &schema) +{ + std::string errors; + auto & value = node.Float(); + if (vstd::contains(schema.Struct(), "maximum")) + { + if (schema["exclusiveMaximum"].Bool()) + { + if (value >= schema["maximum"].Float()) + errors += fail("Value is too large"); + } + else + { + if (value > schema["maximum"].Float()) + errors += fail("Value is too large"); + } + } + + if (vstd::contains(schema.Struct(), "minimum")) + { + if (schema["exclusiveMinimum"].Bool()) + { + if (value <= schema["minimum"].Float()) + errors += fail("Value is too small"); + } + else + { + if (value < schema["minimum"].Float()) + errors += fail("Value is too small"); + } + } + + if (vstd::contains(schema.Struct(), "multipleOf")) + { + double result = value / schema["multipleOf"].Float(); + if (floor(result) != result) + errors += ("Value is not divisible"); + } + return errors; +} + +//basic schema validation (like checking $schema entry). +std::string JsonValidator::validateRoot(const JsonNode &node, std::string schemaName) +{ + const JsonNode & schema = JsonUtils::getSchema(schemaName); + + usedSchemas.push_back(schemaName.substr(0, schemaName.find('#'))); + auto onExit = vstd::makeScopeGuard([&] + { + usedSchemas.pop_back(); + }); + + if (!schema.isNull()) + return validateNode(node, schema); + else + return fail("Schema not found!"); +} + +std::string JsonValidator::fail(const std::string &message) +{ + std::string errors; + errors += "At "; + BOOST_FOREACH(const JsonNode &path, currentPath) + { + errors += "/"; + if (path.getType() == JsonNode::DATA_STRING) + errors += path.String(); + else + errors += boost::lexical_cast(static_cast(path.Float())); + } + errors += "\n\t Error: " + message + "\n"; + return errors; +} + +bool JsonValidator::validate(const JsonNode &root, std::string schemaName, std::string name) +{ + std::string errors = validateRoot(root, schemaName); + + if (!errors.empty()) + { + tlog3 << "Data in " << name << " is invalid!\n"; + tlog3 << errors; + } + + return errors.empty(); } ///JsonUtils @@ -1113,14 +1360,95 @@ void JsonUtils::unparseBonus( JsonNode &node, const Bonus * bonus ) } } -void JsonUtils::minimize(JsonNode & node, const JsonNode& schema) +void minimizeNode(JsonNode & node, const JsonNode & schema) { - JsonValidator validator(node, schema, true); + assert(schema["type"].String() == "object"); + + BOOST_FOREACH(auto & entry, schema["required"].Vector()) + { + std::string name = entry.String(); + + if (node[name].getType() == JsonNode::DATA_STRUCT) + minimizeNode(node[name], schema["properties"][name]); + + if (vstd::contains(node.Struct(), name) && + node[name] == schema["properties"][name]["default"]) + { + node.Struct().erase(name); + } + } } -void JsonUtils::validate(JsonNode & node, const JsonNode& schema) +void JsonUtils::minimize(JsonNode & node, std::string schemaName) { - JsonValidator validator(node, schema, false); + minimizeNode(node, getSchema(schemaName)); +} + +void maximizeNode(JsonNode & node, const JsonNode & schema) +{ + assert(schema["type"].String() == "object"); + + BOOST_FOREACH(auto & entry, schema["required"].Vector()) + { + std::string name = entry.String(); + + if (node[name].isNull() && + !schema["properties"][name]["default"].isNull()) + { + node[name] = schema["properties"][name]["default"]; + } + if (node[name].getType() == JsonNode::DATA_STRUCT) + maximizeNode(node[name], schema["properties"][name]); + } +} + +void JsonUtils::maximize(JsonNode & node, std::string schemaName) +{ + maximizeNode(node, getSchema(schemaName)); +} + +bool JsonUtils::validate(const JsonNode &node, std::string schemaName, std::string dataName) +{ + JsonValidator validator; + return validator.validate(node, schemaName, dataName); +} + +const JsonNode & getSchemaByName(std::string name) +{ + // cached schemas to avoid loading json data multiple times + static std::map loadedSchemas; + + if (vstd::contains(loadedSchemas, name)) + return loadedSchemas[name]; + + std::string filename = "config/schemas/" + name + ".json"; + + if (CResourceHandler::get()->existsResource(ResourceID(filename))) + { + loadedSchemas[name] = JsonNode(ResourceID(filename)); + return loadedSchemas[name]; + } + + tlog0 << "Error: missing schema with name " << name << "!\n"; + assert(0); + return nullNode; +} + +const JsonNode & JsonUtils::getSchema(std::string URI) +{ + std::vector segments; + + boost::split(segments, URI, boost::is_any_of(":#")); + + segments.resize(3);//in case if last section (after #) was not present + + if (segments[0] != "vcmi") + { + tlog0 << "Error: unsupported URI protocol for schema: " << segments[0] << "\n"; + return nullNode; + } + + return getSchemaByName(segments[1]).resolvePointer(segments[2]); } void JsonUtils::merge(JsonNode & dest, JsonNode & source) @@ -1133,7 +1461,7 @@ void JsonUtils::merge(JsonNode & dest, JsonNode & source) switch (source.getType()) { - break; case JsonNode::DATA_NULL: dest.setType(JsonNode::DATA_NULL); + break; case JsonNode::DATA_NULL: dest.clear(); break; case JsonNode::DATA_BOOL: std::swap(dest.Bool(), source.Bool()); break; case JsonNode::DATA_FLOAT: std::swap(dest.Float(), source.Float()); break; case JsonNode::DATA_STRING: std::swap(dest.String(), source.String()); diff --git a/lib/JsonNode.h b/lib/JsonNode.h index a3e43ab3d..0e177fc51 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -63,38 +63,38 @@ public: bool operator == (const JsonNode &other) const; bool operator != (const JsonNode &other) const; - //Convert node to another type. Converting to NULL will clear all data + /// Convert node to another type. Converting to NULL will clear all data void setType(JsonType Type); JsonType getType() const; bool isNull() const; + /// removes all data from node and sets type to null + void clear(); - //non-const accessors, node will change type on type mismatch + /// non-const accessors, node will change type on type mismatch bool & Bool(); double & Float(); std::string & String(); JsonVector & Vector(); JsonMap & Struct(); - //const accessors, will cause assertion failure on type mismatch + /// const accessors, will cause assertion failure on type mismatch const bool & Bool() const; const double & Float() const; const std::string & String() const; const JsonVector & Vector() const; const JsonMap & Struct() const; + /// returns resolved "json pointer" (string in format "/path/to/node") + const JsonNode & resolvePointer(const std::string & jsonPointer) const; + JsonNode & resolvePointer(const std::string & jsonPointer); + /// convert json tree into specified type. Json tree must have same type as Type /// Valid types: bool, string, any numeric, map and vector /// example: convertTo< std::map< std::vector > >(); template Type convertTo() const; - /// Similar to convertTo but will assign data only if node is not null - /// Othervice original data will be preserved - /// Returns true if data was assigned - template - bool loadTo(Type & data) const; - //operator [], for structs only - get child node by name JsonNode & operator[](std::string child); const JsonNode & operator[](std::string child) const; @@ -159,11 +159,28 @@ namespace JsonUtils */ DLL_LINKAGE JsonNode assembleFromFiles(std::vector files); - /// removes all nodes that are identical to default entry in schema - DLL_LINKAGE void minimize(JsonNode & node, const JsonNode& schema); + /** + * @brief removes all nodes that are identical to default entry in schema + * @param node - JsonNode to minimize + * @param schemaName - name of schema to use + * @note for minimizing data must be valid against given schema + */ + DLL_LINKAGE void minimize(JsonNode & node, std::string schemaName); + /// opposed to minimize, adds all missing, required entries that have default value + DLL_LINKAGE void maximize(JsonNode & node, std::string schemaName); - /// check schema - DLL_LINKAGE void validate(JsonNode & node, const JsonNode& schema); + /** + * @brief validate node against specified schema + * @param node - JsonNode to check + * @param schemaName - name of schema to use + * @param dataName - some way to identify data (printed in console in case of errors) + * @returns true if data in node fully compilant with schema + */ + DLL_LINKAGE bool validate(const JsonNode & node, std::string schemaName, std::string dataName); + + /// get schema by json URI: vcmi:# + /// example: schema "vcmi:settings" is used to check user settings + DLL_LINKAGE const JsonNode & getSchema(std::string URI); } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -345,29 +362,42 @@ namespace JsonDetail JsonNode parse(std::string fileName); }; - //Internal class for Json validation, used automaticaly in JsonNode constructor. Behaviour: - // - "schema" entry from root node is used for validation and will be removed - // - any missing entries will be replaced with default value from schema (if present) - // - if entry uses different type than defined in schema it will be removed - // - entries nod described in schema will be kept unchanged + //Internal class for Json validation. Mostly compilant with json-schema v4 draft class JsonValidator { - std::string errors; // Contains description of all encountered errors - std::list currentPath; // path from root node to current one - bool minimize; + // path from root node to current one. + // JsonNode is used as variant - either string (name of node) or as float (index in list) + std::vector currentPath; + // Stack of used schemas. Last schema is the one used currently. + // May contain multiple items in case if remote references were found + std::vector usedSchemas; - bool validateType(JsonNode &node, const JsonNode &schema, JsonNode::JsonType type); - bool validateSchema(JsonNode::JsonType &type, const JsonNode &schema); - bool validateNode(JsonNode &node, const JsonNode &schema, const std::string &name); - bool validateItems(JsonNode &node, const JsonNode &schema); - bool validateProperties(JsonNode &node, const JsonNode &schema); + /// helpers for other validation methods + std::string validateVectorItem(const JsonVector items, const JsonNode & schema, const JsonNode & additional, size_t index); + std::string validateStructItem(const JsonNode &node, const JsonNode &schema, const JsonNode & additional, std::string nodeName); - bool addMessage(const std::string &message); + std::string validateEnum(const JsonNode &node, const JsonVector &enumeration); + std::string validateNodeType(const JsonNode &node, const JsonNode &schema); + std::string validatesSchemaList(const JsonNode &node, const JsonNode &schemas, std::string errorMsg, std::function isValid); + + /// contains all type-independent checks + std::string validateNode(const JsonNode &node, const JsonNode &schema); + + /// type-specific checks + std::string validateVector(const JsonNode &node, const JsonNode &schema); + std::string validateStruct(const JsonNode &node, const JsonNode &schema); + std::string validateString(const JsonNode &node, const JsonNode &schema); + std::string validateNumber(const JsonNode &node, const JsonNode &schema); + + /// validation of root node of both schema and input data + std::string validateRoot(const JsonNode &node, std::string schemaName); + + /// add error message to list and return false + std::string fail(const std::string &message); public: - // validate node with "schema" entry - JsonValidator(JsonNode &root, bool minimize=false); - // validate with external schema - JsonValidator(JsonNode &root, const JsonNode &schema, bool minimize=false); + + /// returns true if parsed data is fully compilant with schema + bool validate(const JsonNode &root, std::string schemaName, std::string name); }; } // namespace JsonDetail @@ -377,11 +407,3 @@ Type JsonNode::convertTo() const { return JsonDetail::JsonConverter::convert(*this); } - -template -bool JsonNode::loadTo(Type & data) const -{ - if (!isNull()) - data = convertTo(); - return !isNull(); -} diff --git a/lib/ScopeGuard.h b/lib/ScopeGuard.h new file mode 100644 index 000000000..6c57a0ca0 --- /dev/null +++ b/lib/ScopeGuard.h @@ -0,0 +1,47 @@ +/* + * ScopeGuard.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 + + +namespace vstd +{ + template + class ScopeGuard + { + bool fire; + Func f; + + explicit ScopeGuard(ScopeGuard&); + ScopeGuard& operator=(ScopeGuard&); + public: + ScopeGuard(ScopeGuard &&other): + fire(false), + f(other.f) + { + std::swap(fire, other.fire); + } + + explicit ScopeGuard(Func && f): + fire(true), + f(std::forward(f)) + {} + ~ScopeGuard() + { + f(); + } + }; + + template + ScopeGuard makeScopeGuard(Func&& exitScope) + { + return ScopeGuard(std::forward(exitScope)); + } +} diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 201bb2186..00b5e0c80 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -92,7 +92,7 @@ template void createHandler(Handler *&handler, const std::string void LibClasses::init() { CStopWatch pomtime; - + createHandler(bth, "Bonus type", pomtime); createHandler(generaltexth, "General text", pomtime); @@ -111,7 +111,6 @@ void LibClasses::init() createHandler(spellh, "Spell", pomtime); - modh->loadActiveMods(); modh->reload(); //FIXME: make sure that everything is ok after game restart