added json validation for saves

This commit is contained in:
StevenRS11 2014-06-20 14:04:07 -04:00
parent dccb12116d
commit eeb5f9aea1
3 changed files with 813 additions and 271 deletions

View file

@ -1,37 +1,44 @@
package StevenDimDoors.mod_pocketDim.saving; package StevenDimDoors.mod_pocketDim.saving;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import StevenDimDoors.mod_pocketDim.Point3D;
import StevenDimDoors.mod_pocketDim.util.BaseConfigurationProcessor;
import StevenDimDoors.mod_pocketDim.util.ConfigurationProcessingException;
import StevenDimDoors.mod_pocketDim.util.JSONValidator;
import StevenDimDoors.mod_pocketDim.util.Point4D;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonToken;
import StevenDimDoors.mod_pocketDim.Point3D;
import StevenDimDoors.mod_pocketDim.core.DDLock;
import StevenDimDoors.mod_pocketDim.util.BaseConfigurationProcessor;
import StevenDimDoors.mod_pocketDim.util.ConfigurationProcessingException;
import StevenDimDoors.mod_pocketDim.util.Point4D;
public class DimDataProcessor extends BaseConfigurationProcessor<PackedDimData> public class DimDataProcessor extends BaseConfigurationProcessor<PackedDimData>
{ {
private static final String JSON_SCHEMA_PATH = "/assets/dimdoors/text/Dim_Data_Schema.json";
private static final JsonParser jsonParser = new JsonParser();
@Override @Override
public PackedDimData readFromStream(InputStream inputStream) public PackedDimData readFromStream(InputStream inputStream)
throws ConfigurationProcessingException throws ConfigurationProcessingException
{ {
try try
{ {
//read in the json save file represeting a single dimension
JsonReader reader = new JsonReader(new InputStreamReader(inputStream, "UTF-8")); JsonReader reader = new JsonReader(new InputStreamReader(inputStream, "UTF-8"));
PackedDimData data = this.createDImDataFromJson(reader); PackedDimData data = this.readDimDataJson(reader);
reader.close(); reader.close();
return data; return data;
} }
catch (IOException e) catch (Exception e)
{ {
e.printStackTrace(); e.printStackTrace();
throw new ConfigurationProcessingException("Could not read packedDimData"); throw new ConfigurationProcessingException("Could not read packedDimData");
@ -43,284 +50,46 @@ public class DimDataProcessor extends BaseConfigurationProcessor<PackedDimData>
public void writeToStream(OutputStream outputStream, PackedDimData data) public void writeToStream(OutputStream outputStream, PackedDimData data)
throws ConfigurationProcessingException throws ConfigurationProcessingException
{ {
/** Print out dimData using the GSON built in serializer. I dont feel bad doing this because //create a json object from a packedDimData instance
* 1- We can read it
* 2- We are manually reading the data in.
* 3- The error messages tell us exactly where its failing, so its easy to fix
*/
GsonBuilder gsonBuilder = new GsonBuilder(); GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.setPrettyPrinting().create(); Gson gson = gsonBuilder.setPrettyPrinting().create();
JsonElement ele = gson.toJsonTree(data);
try try
{ {
outputStream.write(gson.toJson(data).getBytes("UTF-8")); //ensure our json object corresponds to our schema
validateJson(ele);
outputStream.write(data.toString().getBytes("UTF-8"));
outputStream.close(); outputStream.close();
} }
catch (IOException e) catch (Exception e)
{ {
// not sure if this is kosher, we need it to explode, but not by throwing the IO exception. // not sure if this is kosher, we need it to explode, but not by throwing the IO exception.
throw new ConfigurationProcessingException("Incorrectly formatted save data"); throw new ConfigurationProcessingException("Incorrectly formatted save data");
} }
} }
public PackedDimData readDimDataJson(JsonReader reader) throws IOException
{
JsonElement ele = jsonParser.parse(reader);
this.validateJson(ele);
GsonBuilder gsonBuilder = new GsonBuilder();
return gsonBuilder.create().fromJson(ele, PackedDimData.class);
}
/** /**
* Nightmare method that takes a JsonReader pointed at a serialized instance of PackedDimData * checks our json against the dim data schema
* @param reader * @param data
* @return * @return
* @throws IOException * @throws IOException
*/ */
public PackedDimData createDImDataFromJson(JsonReader reader) throws IOException public boolean validateJson(JsonElement data) throws IOException
{ {
int ID; InputStream in = this.getClass().getResourceAsStream(JSON_SCHEMA_PATH);
boolean IsDungeon; JsonReader reader = new JsonReader(new InputStreamReader(in));
boolean IsFilled; JSONValidator.validate((JsonObject) jsonParser.parse(reader), data);
int Depth; reader.close();
int PackDepth; in.close();
int ParentID; return true;
int RootID;
PackedDungeonData Dungeon = null;
Point3D Origin;
int Orientation;
List<Integer> ChildIDs;
List<PackedLinkData> Links;
List<PackedLinkTail> Tails = new ArrayList<PackedLinkTail>();
reader.beginObject();
reader.nextName();
if (reader.nextLong() != PackedDimData.SAVE_DATA_VERSION_ID)
{
throw new IOException("Save data version mismatch");
} }
reader.nextName();
ID = reader.nextInt();
reader.nextName();
IsDungeon = reader.nextBoolean();
reader.nextName();
IsFilled = reader.nextBoolean();
reader.nextName();
Depth = reader.nextInt();
reader.nextName();
PackDepth = reader.nextInt();
reader.nextName();
ParentID=reader.nextInt();
reader.nextName();
RootID= reader.nextInt();
if(reader.nextName().equals("DungeonData"))
{
Dungeon = createDungeonDataFromJson(reader);
reader.nextName();
}
Origin = createPointFromJson(reader);
reader.nextName();
Orientation = reader.nextInt();
reader.nextName();
ChildIDs = this.createIntListFromJson(reader);
reader.nextName();
Links = this.createLinksListFromJson(reader);
return new PackedDimData(ID, Depth, PackDepth, ParentID, RootID, Orientation, IsDungeon, IsFilled, Dungeon, Origin, ChildIDs, Links, Tails);
}
private Point3D createPointFromJson(JsonReader reader) throws IOException
{
reader.beginObject();
reader.nextName();
int x = reader.nextInt();
reader.nextName();
int y = reader.nextInt();
reader.nextName();
int z = reader.nextInt();
reader.endObject();
return new Point3D(x,y,z);
}
private Point4D createPoint4DFromJson(JsonReader reader) throws IOException
{
reader.beginObject();
reader.nextName();
int x = reader.nextInt();
reader.nextName();
int y = reader.nextInt();
reader.nextName();
int z = reader.nextInt();
reader.nextName();
int dimension = reader.nextInt();
reader.endObject();
return new Point4D(x,y,z,dimension);
}
private List<Integer> createIntListFromJson(JsonReader reader) throws IOException
{
List<Integer> list = new ArrayList<Integer>();
reader.beginArray();
while (reader.peek() != JsonToken.END_ARRAY)
{
list.add(reader.nextInt());
}
reader.endArray();
return list;
}
private List<PackedLinkData> createLinksListFromJson(JsonReader reader) throws IOException
{
List<PackedLinkData> list = new ArrayList<PackedLinkData>();
reader.beginArray();
while (reader.peek() != JsonToken.END_ARRAY)
{
list.add(createLinkDataFromJson(reader));
}
reader.endArray();
return list;
}
private PackedLinkData createLinkDataFromJson(JsonReader reader) throws IOException
{
DDLock lock = null;
Point4D source;
Point3D parent;
PackedLinkTail tail;
int orientation;
List<Point3D> children = new ArrayList<Point3D>();
reader.beginObject();
reader.nextName();
source = this.createPoint4DFromJson(reader);
reader.nextName();
parent = this.createPointFromJson(reader);
reader.nextName();
tail = this.createLinkTailFromJson(reader);
reader.nextName();
orientation = reader.nextInt();
reader.nextName();
reader.beginArray();
while (reader.peek() != JsonToken.END_ARRAY)
{
children.add(this.createPointFromJson(reader));
}
reader.endArray();
if(reader.peek()== JsonToken.NAME)
{
lock = this.createLockFromJson(reader);
}
reader.endObject();
return new PackedLinkData(source, parent, tail, orientation, children, lock);
}
private PackedDungeonData createDungeonDataFromJson(JsonReader reader) throws IOException
{
int Weight;
boolean IsOpen;
boolean IsInternal;
String SchematicPath;
String SchematicName;
String DungeonTypeName;
String DungeonPackName;
reader.beginObject();
@SuppressWarnings("unused")
JsonToken test = reader.peek();
if(reader.peek() == JsonToken.END_OBJECT)
{
return null;
}
reader.nextName();
Weight=reader.nextInt();
reader.nextName();
IsOpen=reader.nextBoolean();
reader.nextName();
IsInternal=reader.nextBoolean();
reader.nextName();
SchematicPath=reader.nextString();
reader.nextName();
SchematicName=reader.nextString();
reader.nextName();
DungeonTypeName=reader.nextString();
reader.nextName();
DungeonPackName=reader.nextString();
reader.endObject();
return new PackedDungeonData(Weight, IsOpen, IsInternal, SchematicPath, SchematicName, DungeonTypeName, DungeonPackName);
}
private PackedLinkTail createLinkTailFromJson(JsonReader reader) throws IOException
{
Point4D destination = null;
int linkType;
reader.beginObject();
reader.nextName();
@SuppressWarnings("unused")
JsonToken test = reader.peek();
if (reader.peek() == JsonToken.BEGIN_OBJECT)
{
destination = this.createPoint4DFromJson(reader);
reader.nextName();
}
linkType = reader.nextInt();
reader.endObject();
return new PackedLinkTail(destination, linkType);
}
private DDLock createLockFromJson(JsonReader reader) throws IOException
{
reader.nextName();
reader.beginObject();
reader.nextName();
boolean locked = reader.nextBoolean();
reader.nextName();
int key = reader.nextInt();
reader.endObject();
return new DDLock(locked, key);
}
} }

View file

@ -0,0 +1,544 @@
package StevenDimDoors.mod_pocketDim.util;
import static java.util.Collections.singleton;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
public class JSONValidator
{
static final public String TYPE = "type";
static final public String ANY = "any";
static final public String PROPERTIES = "properties";
static final public String OPTIONAL = "optional";
static final public String ADDITIONAL_PROPERTIES = "additionalProperties";
static final public String MIN_LENGTH = "minLength";
static final public String MAX_LENGTH = "maxLength";
static final public String MINIMUM = "minimum";
static final public String MAXIMUM = "maximum";
static final public String PATTERN = "pattern";
static final public String ITEMS = "items";
static final public String ENUM = "enum";
static final public String REQUIRED = "required";
JsonObject schema;
public JsonObject getSchema()
{
return schema;
}
public JSONValidator(JsonObject schema)
{
this.schema = schema;
}
static class WrongType extends JsonParseException
{
/**
*
*/
private static final long serialVersionUID = 1L;
WrongType(String msg)
{
super(msg);
}
static WrongType generate(String path, Set<Type> types, Type found)
{
boolean first = true;
String typeList = "'unknown'";
for (Type type : types)
{
if (first)
{
typeList = "'" + type.getTypeString() + "'";
first = false;
}
else
{
typeList += " or '" + type.getTypeString() + "'";
}
}
return new WrongType("Invalid: Expected type " + typeList + " at '" + path + "', but " + "found type '" + found.getTypeString() + "'");
}
}
static enum Type
{
STRING("string"), NUMBER("number"), INTEGER("integer"), BOOLEAN("boolean"), OBJECT("object"), ARRAY("array"), NULL("null");
String typeString;
Type(String typeString)
{
this.typeString = typeString;
}
public String getTypeString()
{
return typeString;
}
}
static Set<Type> anyTypeSet()
{
HashSet<Type> hashSet = new HashSet<Type>();
hashSet.add(Type.STRING);
hashSet.add(Type.NUMBER);
hashSet.add(Type.INTEGER);
hashSet.add(Type.BOOLEAN);
hashSet.add(Type.OBJECT);
hashSet.add(Type.ARRAY);
hashSet.add(Type.NULL);
return hashSet;
}
static Set<Type> getSimpleType(String path, String type)
{
for (Type t : Type.values())
{
if (t.getTypeString().equals(type))
{
if (t != Type.NUMBER)
{
return singleton(t);
}
else
{
HashSet<Type> set = new HashSet<Type>();
set.add(Type.NUMBER);
set.add(Type.INTEGER);
return set;
}
}
}
if (ANY.equals(type))
{
return anyTypeSet();
}
// Unknown type, spec says to allow any.
return anyTypeSet();
}
static Set<Type> getTypeSet(String path, JsonObject schema) throws JsonParseException
{
JsonElement typeElement = schema.get(TYPE);
if (typeElement == null)
{
// Spec says that a missing type object means accept any type.
return anyTypeSet();
}
if (typeElement.isJsonPrimitive())
{
JsonPrimitive primitive = typeElement.getAsJsonPrimitive();
if (primitive.isString())
{
return getSimpleType(path, primitive.getAsString());
}
}
if (typeElement.isJsonArray())
{
HashSet<Type> set = new HashSet<Type>();
JsonArray array = typeElement.getAsJsonArray();
for (JsonElement element : array)
{
if (element.isJsonPrimitive())
{
JsonPrimitive primitive = element.getAsJsonPrimitive();
if (primitive.isString())
{
set.addAll(getSimpleType(path, primitive.getAsString()));
}
}
// Unknown type. Accept all.
return anyTypeSet();
}
}
// Don't know what this is, assume any.
return anyTypeSet();
}
static Type getType(JsonElement element)
{
if (element.isJsonArray())
{
return Type.ARRAY;
}
if (element.isJsonObject())
{
return Type.OBJECT;
}
if (element.isJsonNull())
{
return Type.NULL;
}
JsonPrimitive primitive = element.getAsJsonPrimitive();
if (primitive.isString())
{
return Type.STRING;
}
if (primitive.isBoolean())
{
return Type.BOOLEAN;
}
if (primitive.isNumber())
{
BigDecimal decimal = primitive.getAsBigDecimal();
int scale = decimal.scale();
if (scale > 0)
{
return Type.NUMBER;
}
else
{
return Type.INTEGER;
}
}
// Don't know. Punt and call it a string.
return Type.STRING;
}
static void validateObject(String path, JsonObject schema, JsonObject obj) throws JsonParseException
{
Set<String> propertiesSeen = new HashSet<String>();
JsonArray required = schema.getAsJsonArray(REQUIRED);
JsonObject properties = schema.getAsJsonObject(PROPERTIES);
if (properties == null)
{
return;
}
Set<Map.Entry<String, JsonElement>> propertySet = properties.entrySet();
ArrayList<String> requiredFields = new ArrayList<String>();
for(JsonElement st : required.getAsJsonArray())
{
requiredFields.add(st.getAsString());
}
for (Map.Entry<String, JsonElement> property : propertySet)
{
String name = property.getKey();
String newPath = path + "['" + name + "']";
JsonElement element = property.getValue();
propertiesSeen.add(name);
if (!element.isJsonObject())
{
throw new JsonParseException("Bad Schema: property definition not an object at '" + newPath + "'");
}
JsonObject definition = element.getAsJsonObject();
JsonElement newTarget = obj.get(name);
if (newTarget == null)
{
JsonPrimitive optional = definition.getAsJsonPrimitive(OPTIONAL);
boolean needed = ((optional==null) && requiredFields.contains(name)) || (optional != null && !optional.getAsBoolean());
if (needed)
{
throw new JsonParseException("Invalid: Required property '" + newPath + "' not found");
}
}
else
{
validate(newPath, definition, newTarget);
}
}
JsonElement additionalProperties = schema.get(ADDITIONAL_PROPERTIES);
JsonObject additionalSchema = null;
if (additionalProperties == null)
{
additionalSchema = new JsonObject();
}
else
{
if (additionalProperties.isJsonObject())
{
additionalSchema = additionalProperties.getAsJsonObject();
}
}
/*
* if (additionalSchema == null) {
* logger.debug("No additional schema for '"+path+"'"); } else {
* logger.debug("Additional schema for '"+path+"': "+
* additionalSchema.toString()); }
*/
Set<Map.Entry<String, JsonElement>> objectProperties = obj.entrySet();
for (Map.Entry<String, JsonElement> property : objectProperties)
{
String name = property.getKey();
String newPath = path + "['" + name + "']";
if (!propertiesSeen.contains(name))
{
if (additionalSchema == null)
{
throw new JsonParseException("Invalid: Found additional property '" + newPath + "'");
}
validate(newPath, additionalSchema, property.getValue());
}
}
}
static Integer getInt(String path, String attributeName, JsonObject schema) throws JsonParseException
{
JsonElement attributeElement = schema.get(attributeName);
if (attributeElement == null)
{
return null;
}
if (!attributeElement.isJsonPrimitive())
{
throw new JsonParseException("Bad Schema: '" + attributeName + "' attribute is not an integer at '" + path + "'");
}
JsonPrimitive attributePrimitive = attributeElement.getAsJsonPrimitive();
if (!attributePrimitive.isNumber())
{
throw new JsonParseException("Bad Schema: '" + attributeName + "' attribute is not an integer at '" + path + "'");
}
return attributePrimitive.getAsInt();
}
static String getString(String path, String attributeName, JsonObject schema) throws JsonParseException
{
JsonElement attributeElement = schema.get(attributeName);
if (attributeElement == null)
{
return null;
}
if (!attributeElement.isJsonPrimitive())
{
throw new JsonParseException("Bad Schema: '" + attributeName + "' attribute is not a string at '" + path + "'");
}
JsonPrimitive attributePrimitive = attributeElement.getAsJsonPrimitive();
if (!attributePrimitive.isString())
{
throw new JsonParseException("Bad Schema: '" + attributeName + "' attribute is not a string at '" + path + "'");
}
return attributePrimitive.getAsString();
}
static BigDecimal getBigDecimal(String path, String attributeName, JsonObject schema) throws JsonParseException
{
JsonElement attributeElement = schema.get(attributeName);
if (attributeElement == null)
{
return null;
}
if (!attributeElement.isJsonPrimitive())
{
throw new JsonParseException("Bad Schema: '" + attributeName + "' attribute is not a number at '" + path + "'");
}
JsonPrimitive attributePrimitive = attributeElement.getAsJsonPrimitive();
if (!attributePrimitive.isNumber())
{
throw new JsonParseException("Bad Schema: '" + attributeName + "' attribute is not a number at '" + path + "'");
}
return attributePrimitive.getAsBigDecimal();
}
static void validateString(String path, JsonObject schema, String str) throws JsonParseException
{
Integer minLength = getInt(path, MIN_LENGTH, schema);
Integer maxLength = getInt(path, MAX_LENGTH, schema);
if ((minLength != null) && (str.length() < minLength))
{
throw new JsonParseException("Invalid: String '" + path + "' is too short. The string needs to be more than " + minLength + " characters");
}
if ((maxLength != null) && (str.length() > maxLength))
{
throw new JsonParseException("Invalid: String '" + path + "' is too long. The string needs to be less than " + maxLength + " characters");
}
String pattern = getString(path, PATTERN, schema);
if ((pattern != null) && (!str.matches(pattern)))
{
throw new JsonParseException("Invalid: String '" + path + "' does not match pattern '" + pattern + "'");
}
}
static void validateTuple(String path, JsonArray tupleSchema, JsonObject additionalSchema, JsonArray array) throws JsonParseException
{
return;
}
static void validateArray(String path, JsonObject schema, JsonArray array) throws JsonParseException
{
JsonElement additionalProperties = schema.get(ADDITIONAL_PROPERTIES);
JsonObject additionalSchema = null;
if (additionalProperties == null)
{
additionalSchema = new JsonObject();
}
else
{
if (additionalProperties.isJsonObject())
{
additionalSchema = additionalProperties.getAsJsonObject();
}
}
JsonElement itemsElement = schema.get(ITEMS);
if (itemsElement == null)
{
return;
}
if (itemsElement.isJsonArray())
{
validateTuple(path, itemsElement.getAsJsonArray(), additionalSchema, array);
return;
}
JsonObject itemsSchema = null;
if (itemsElement.isJsonObject())
{
itemsSchema = itemsElement.getAsJsonObject();
}
else
{
// Bogus items parameter, assume everything is valid.
itemsSchema = new JsonObject();
}
int i = 0;
for (JsonElement element : array)
{
++i;
String curPath = path + "[" + i + "]";
validate(curPath, itemsSchema, element);
}
}
static void validateEnum(String path, JsonObject schema, JsonElement element) throws JsonParseException
{
JsonElement enumElement = schema.get(ENUM);
if (enumElement == null)
{
return;
}
if (!enumElement.isJsonArray())
{}
JsonArray enumArray = enumElement.getAsJsonArray();
for (JsonElement curElement : enumArray)
{
if (element.equals(curElement))
{
// We found a valid value.
return;
}
}
throw new JsonParseException("Invalid: Property '" + path + "' is not one of the enum values.");
}
static void validateNumber(String path, JsonObject schema, BigDecimal number) throws JsonParseException
{
BigDecimal minimum = getBigDecimal(path, MINIMUM, schema);
if (minimum != null)
{
if (number.compareTo(minimum) < 0)
{
throw new JsonParseException("Invalid: Property '" + path + "' has a value of '" + number + "' which is less than the minimum of '" + minimum
+ "'.");
}
}
BigDecimal maximum = getBigDecimal(path, MAXIMUM, schema);
if (maximum != null)
{
if (number.compareTo(maximum) > 0)
{
throw new JsonParseException("Invalid: Property '" + path + "' has a value of '" + number + "' which is greater than the maximum of '"
+ maximum + "'.");
}
}
}
static void validate(String path, JsonObject schema, JsonElement element) throws JsonParseException
{
Set<Type> typeSet = getTypeSet(path, schema);
Type type = getType(element);
if (!typeSet.contains(type))
{
throw WrongType.generate(path, typeSet, type);
}
switch (type)
{
case BOOLEAN:
case NULL:
break;
case NUMBER:
case INTEGER:
validateNumber(path, schema, element.getAsBigDecimal());
break;
case ARRAY:
validateArray(path, schema, element.getAsJsonArray());
break;
case STRING:
validateString(path, schema, element.getAsString());
break;
case OBJECT:
validateObject(path, schema, element.getAsJsonObject());
break;
default:
// Unknown type
throw new JsonParseException("Internal Error");
}
validateEnum(path, schema, element);
}
static public void validate(JsonObject schema, JsonElement element) throws JsonParseException
{
validate("$", schema, element);
}
public void validate(JsonElement element) throws JsonParseException
{
validate(getSchema(), element);
}
}

View file

@ -0,0 +1,229 @@
{
"type":"object",
"$schema": "http://json-schema.org/draft-04/schema",
"description": "A serialized Dim Data object",
"properties":{
"ChildIDs": {
"type":"array",
"items": {
"type": "number"
}
},
"Depth": {
"type":"number"
},
"ID": {
"type":"number"
},
"IsDungeon": {
"type":"boolean"
},
"IsFilled": {
"type":"boolean"
},
"DungeonData": {
"type": "object",
"properties": {
"Weight": {
"type": "number"
},
"IsOpen": {
"type": "boolean"
},
"IsInternal": {
"type": "boolean"
},
"SchematicPath": {
"type": "string"
},
"SchematicName": {
"type": "string"
},
"DungeonTypeName": {
"type": "string"
},
"DungeonPackName": {
"type": "string"
}
},
"required": [
"Weight",
"IsOpen",
"IsInternal",
"SchematicPath",
"SchematicName",
"DungeonTypeName",
"DungeonPackName"
]
},
"Links": {
"type":"array",
"items": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items":{
"type": "number"
}
},
"orientation": {
"type": "number"
},
"source": {
"type": "object",
"properties": {
"x": {
"type": "integer"
},
"y": {
"type": "integer"
},
"z": {
"type": "integer"
},
"dimension": {
"type": "integer"
}
},
"required": [
"x",
"y",
"z",
"dimension"
]
},
"parent": {
"type": "object",
"properties": {
"x": {
"type": "integer"
},
"y": {
"type": "integer"
},
"z": {
"type": "integer"
}
},
"required": [
"x",
"y",
"z"
]
},
"tail": {
"type": "object",
"properties": {
"linkType" : {
"type": "number"
},
"destination":{
"type": "object",
"properties": {
"x": {
"type": "integer"
},
"y": {
"type": "integer"
},
"z": {
"type": "integer"
},
"dimension": {
"type": "integer"
}
},
"required": [
"x",
"y",
"z",
"dimension"
]
}
},
"required": [
"linkType"
]
},
"lock":{
"type": "object",
"properties": {
"lockState": {
"type": "boolean"
},
"lockKey": {
"type": "number"
}
},
"required": [
"lockState",
"lockKey"
]
}
},
"required": [
"children",
"orientation",
"source",
"parent",
"tail"
]
}
},
"Orientation": {
"type":"number"
},
"Origin": {
"type":"object",
"properties":{
"x": {
"type":"number"
},
"y": {
"type":"number"
},
"z": {
"type":"number"
}
},
"required": [
"x",
"y",
"z"
]
},
"PackDepth": {
"type":"number"
},
"ParentID": {
"type":"number"
},
"RootID": {
"type":"number"
},
"SAVE_DATA_VERSION_ID_INSTANCE": {
"type":"number"
},
"Tails": {
"type":"array"
}
},
"required": ["Tails",
"SAVE_DATA_VERSION_ID_INSTANCE",
"RootID",
"ParentID",
"PackDepth",
"Origin",
"Orientation",
"Links",
"IsFilled",
"IsDungeon",
"ID",
"Depth",
"ChildIDs"
]
}