1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-24 22:14:36 +02:00

- 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.
This commit is contained in:
Ivan Savenko 2013-04-02 17:06:43 +00:00
parent c8bb363a45
commit c927913f5f
25 changed files with 2186 additions and 339 deletions

View File

@ -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);
}

View File

@ -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" : {}
}
}

View File

@ -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" :
{

View File

@ -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"
}
}
}

92
config/schemas/bonus.json Normal file
View File

@ -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",
}
}
}

View File

@ -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" }
}
}
}
}

250
config/schemas/faction.json Normal file
View File

@ -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"
}
}
}
}
}

151
config/schemas/hero.json Normal file
View File

@ -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."
}
}
}
}
}
}
}

View File

@ -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"
}
}
}
}

78
config/schemas/mod.json Normal file
View File

@ -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"
}
}
}
}
}
}
}

View File

@ -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"
}
}
}
}
}

View File

@ -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"}
}
}
}
}

View File

@ -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"
}
}
}
}
}
}
}

View File

@ -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"
}
}
}

View File

@ -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();

View File

@ -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<std::string> &changedPath)
@ -69,10 +70,10 @@ void SettingsStorage::invalidateNode(const std::vector<std::string> &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;

View File

@ -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

View File

@ -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 <TModID> & 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 <TModID> & 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<std::string> 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<std::string> availableMods)
file << modConfig;
}
std::vector<std::string> CModHandler::getActiveMods()
{
return activeMods;
}
template<typename Handler>
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<std::vector<std::string> >());
JsonNode config = JsonUtils::assembleFromFiles(source[listName].convertTo<std::vector<std::string> >());
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

View File

@ -75,7 +75,7 @@ class DLL_LINKAGE CModHandler
bool hasCircularDependency(TModID mod, std::set <TModID> currentList = std::set <TModID>()) 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 <TModID> resolveDependencies(std::vector<TModID> input) const;
// helper for loadActiveMods. Loads content from list of files
template<typename Handler>
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

View File

@ -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

View File

@ -420,7 +420,8 @@ std::vector<std::string> 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;

View File

@ -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("<unknown>");
JsonValidator validator(*this);
}
JsonNode::JsonNode(ResourceID && fileURI):
@ -45,8 +45,6 @@ JsonNode::JsonNode(ResourceID && fileURI):
JsonParser parser(reinterpret_cast<char*>(file.first.get()), file.second);
*this = parser.parse(fileURI.getName());
JsonValidator validator(*this);
}
JsonNode::JsonNode(const JsonNode &copy):
@ -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<typename Node>
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<size_t>(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<typename Iterator>
@ -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<std::string, JsonNode::JsonType> 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<std::string, JsonNode::JsonType>::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<bool(size_t)> 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<<errors;
// othervice check against schema in additional items field
if (additional.getType() == JsonNode::DATA_STRUCT)
return validateNode(items[index], additional);
// or, additionalItems field can be bool which indicates if such items are allowed
// default = false, so case if additionalItems is not present will be handled as well
if (!additional.Bool())
return fail("Unknown entry found");
return "";
}
JsonValidator::JsonValidator(JsonNode &root, const JsonNode &schema, bool Minimize):
minimize(Minimize)
//Checks "items" entry from schema (type-specific check for Vector)
std::string JsonValidator::validateVector(const JsonNode &node, const JsonNode &schema)
{
validateProperties(root, schema);
if (schema.isNull())
addMessage("Schema not found!");
tlog3<<errors;
std::string errors;
auto & vector = node.Vector();
for (size_t i=0; i<vector.size(); i++)
errors += validateVectorItem(vector, schema["items"], schema["additionalItems"], i);
if (vstd::contains(schema.Struct(), "maxItems") && vector.size() > 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<std::string>(static_cast<unsigned>(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<std::string, JsonNode> 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<std::string> 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());

View File

@ -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<int> > >();
template<typename Type>
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<typename Type>
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<std::string> 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:<name of file in schemas directory>#<entry in file, optional>
/// 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<std::string> 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<JsonNode> 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<std::string> 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<bool(size_t)> 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<Type>::convert(*this);
}
template<typename Type>
bool JsonNode::loadTo(Type & data) const
{
if (!isNull())
data = convertTo<Type>();
return !isNull();
}

47
lib/ScopeGuard.h Normal file
View File

@ -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<typename Func>
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<Func>(f))
{}
~ScopeGuard()
{
f();
}
};
template <typename Func>
ScopeGuard<Func> makeScopeGuard(Func&& exitScope)
{
return ScopeGuard<Func>(std::forward<Func>(exitScope));
}
}

View File

@ -92,7 +92,7 @@ template <class Handler> 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